PHP Classes

File: templates/echomail.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/echomail.twig   Download  
File: templates/echomail.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 67,027 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.echomail.title', {}, locale, ['common']) }}{% if echoarea %} - {{ echoarea }}{% endif %} - {{ parent() }}{% endblock %} {% block content %} <!-- Echo area info bar: description + subscribe toggle + post (desktop only, visible only when an echo is selected) --> <div id="echoInfoBar" class="d-none mb-3"> <div class="d-flex align-items-start justify-content-between gap-3 px-3 py-2 border rounded"> <div class="d-flex flex-column" style="min-width:0"> <strong id="echoTitle" class="text-warning small"></strong> <small id="echoDescription" class="text-muted"></small> </div> <div class="d-flex flex-column gap-1 flex-shrink-0"> <button class="btn btn-sm" id="echoSubscribeBtn" onclick="toggleSubscription()"></button> <div class="d-flex gap-1"> <button class="btn btn-sm btn-outline-primary" onclick="composeMessage('echomail')"> {{ t('ui.echomail.post_message', {}, locale, ['common']) }} </button> {% if ai_assistant_enabled %} <button class="btn btn-sm btn-outline-secondary" onclick="openAiAssistant(typeof currentMessageId !== 'undefined' ? currentMessageId : null, 'echomail')"> <i class="fas fa-robot me-1"></i>{{ t('ui.ai_assistant.button', {}, locale, ['common']) }} </button> {% endif %} </div> {% if current_user.is_admin %} <a class="btn btn-sm btn-outline-secondary d-none" id="echoEditAreaBtn" href="/echoareas"> <i class="fas fa-edit me-1"></i>{{ t('ui.echomail.edit_area', {}, locale, ['common']) }} </a> {% endif %} </div> </div> </div> <!-- Mobile Controls --> <div class="d-lg-none mb-4"> <!-- Mobile Echo Areas Accordion --> <div class="accordion" id="mobileControlsAccordion"> <div class="accordion-item"> <h2 class="accordion-header" id="echoAreasHeading"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#echoAreasCollapse"> <i class="fas fa-list me-2"></i> <span id="mobileAccordionText"> {% if echoarea %} {{ t('ui.echomail.viewing_prefix', {}, locale, ['common']) }} {{ echoarea }} {% else %} {{ t('ui.echomail.viewing_all', {}, locale, ['common']) }} {% endif %} </span> </button> </h2> <div id="echoAreasCollapse" class="accordion-collapse collapse" data-bs-parent="#mobileControlsAccordion"> <div class="accordion-body"> <div class="d-flex gap-2 mb-2"> <a href="/echolist" class="btn btn-sm btn-outline-primary flex-fill"> <i class="fa-solid fa-list me-1"></i> {{ t('ui.echomail.full_echo_list', {}, locale, ['common']) }} </a> <a href="/subscriptions" id="mobileManageBtn" class="btn btn-sm btn-outline-secondary flex-fill"> <i class="fa-solid fa-star me-1" id="mobileManageBtnIcon"></i> <span id="mobileManageBtnLabel">{{ t('ui.echomail.manage_subscriptions', {}, locale, ['common']) }}</span> </a> </div> <ul class="nav nav-tabs nav-fill mb-0" id="mobileViewingTabs" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active py-1 small" id="mobileAreaListTab" data-bs-toggle="tab" data-bs-target="#mobileAreaListPane" type="button" role="tab"> {{ t('ui.echomail.tab_area_list', {}, locale, ['common']) }} </button> </li> {% if interests_enabled and has_interests %} <li class="nav-item" role="presentation"> <button class="nav-link py-1 small" id="mobileInterestsTab" data-bs-toggle="tab" data-bs-target="#mobileInterestsPane" type="button" role="tab"> {{ t('ui.echomail.tab_interests', {}, locale, ['common']) }} </button> </li> {% endif %} </ul> <div class="tab-content border border-top-0 rounded-bottom"> <div class="tab-pane fade show active p-0" id="mobileAreaListPane" role="tabpanel"> <div class="px-2 pt-2"> {% if interests_enabled and has_interests %} <select class="form-select form-select-sm mb-2" id="mobileAreaListInterestFilter" onchange="setAreaListInterestFilter(this.value ? parseInt(this.value) : null)"> <option value="">{{ t('ui.echomail.all_subscribed_areas', {}, locale, ['common']) }}</option> </select> {% endif %} <input type="text" class="form-control form-control-sm" id="mobileEchoareaSearchInput" placeholder="{{ t('ui.echomail.search_areas_placeholder', {}, locale, ['common']) }}" onkeyup="searchEchoareas(this.value)"> </div> <div style="max-height: 268px; overflow-y: auto;"> <div id="mobileEchoareasList"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.echomail.loading_areas', {}, locale, ['common']) }} </div> </div> </div> </div> {% if interests_enabled and has_interests %} <div class="tab-pane fade p-0" id="mobileInterestsPane" role="tabpanel"> <div style="max-height: 300px; overflow-y: auto;"> <div id="mobileInterestsList"> <div class="text-center text-muted p-3"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.echomail.loading_areas', {}, locale, ['common']) }} </div> </div> </div> </div> {% endif %} </div> </div> </div> </div> <!-- Mobile Search Accordion --> <div class="accordion-item"> <h2 class="accordion-header" id="mobileSearchHeading"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#mobileSearchCollapse" aria-expanded="false"> <i class="fas fa-search me-2"></i> {{ t('ui.common.search_messages', {}, locale, ['common']) }} </button> </h2> <div id="mobileSearchCollapse" class="accordion-collapse collapse" data-bs-parent="#mobileControlsAccordion"> <div class="accordion-body"> <div class="mb-3"> <input type="text" class="form-control" id="mobileSearchInput" placeholder="{{ t('ui.common.search_messages_placeholder', {}, locale, ['common']) }}"> </div> <button class="btn btn-outline-primary w-100" onclick="searchMessagesFromMobile()"> <i class="fas fa-search"></i> {{ t('ui.common.search', {}, locale, ['common']) }} </button> </div> </div> </div> </div> </div> <div class="row"> <!-- Messages Column --> <div class="col-lg-9"> <div class="card"> <div class="card-header"> <div class="d-flex justify-content-between align-items-center"> <h6 class="mb-0"> {% if echoarea %} {{ echoarea }} Messages {% else %} {{ t('ui.echomail.recent_messages', {}, locale, ['common']) }} {% endif %} </h6> <div class="d-flex align-items-center gap-2"> <div id="bulkActions" class="d-none"> <span class="me-2 text-muted">{{ t('ui.common.selected', {}, locale, ['common']) }}: <span id="selectedCount">0</span></span> <button type="button" class="btn btn-outline-primary btn-sm" onclick="markSelectedAsRead()" disabled> <i class="fas fa-envelope-open"></i> {{ t('ui.common.mark_as_read', {}, locale, ['common']) }} </button> {% if current_user.is_admin %} <button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteSelectedMessages()" disabled> <i class="fas fa-trash"></i> {{ t('ui.netmail.delete_selected', {}, locale, ['common']) }} </button> {% endif %} </div> <div class="btn-group btn-group-sm" role="group"> <button type="button" class="btn btn-outline-secondary" onclick="toggleSelectMode()" id="selectModeBtn"> <i class="fas fa-check-square"></i> {{ t('ui.common.select', {}, locale, ['common']) }} </button> <button type="button" class="btn btn-outline-secondary" onclick="refreshMessages()"> <i class="fas fa-sync-alt"></i> </button> <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown"> <i class="fas fa-sort"></i> </button> <ul class="dropdown-menu"> <li><a class="dropdown-item sort-option" data-sort="date_desc" href="#" onclick="sortMessages('date_desc')">{{ t('ui.common.sort.newest_first', {}, locale, ['common']) }}</a></li> <li><a class="dropdown-item sort-option" data-sort="date_asc" href="#" onclick="sortMessages('date_asc')">{{ t('ui.common.sort.oldest_first', {}, locale, ['common']) }}</a></li> <li><a class="dropdown-item sort-option" data-sort="subject" href="#" onclick="sortMessages('subject')">{{ t('ui.common.sort.by_subject', {}, locale, ['common']) }}</a></li> <li><a class="dropdown-item sort-option" data-sort="author" href="#" onclick="sortMessages('author')">{{ t('ui.common.sort.by_author', {}, locale, ['common']) }}</a></li> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" href="#" onclick="toggleThreading()"> <i class="fas fa-project-diagram"></i> <span id="threadingToggleText">{{ t('ui.common.threading.show_threaded', {}, locale, ['common']) }}</span> </a></li> </ul> </div> </div> </div> <!-- Read/Unread Filter Tabs --> <ul class="nav nav-tabs card-header-tabs mt-2"> <li class="nav-item"> <a class="nav-link active" href="#" onclick="setFilter('all')" id="allTab"> <i class="fas fa-list"></i> {{ t('ui.common.all_messages', {}, locale, ['common']) }} <span class="badge bg-secondary ms-1" id="allCount">0</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#" onclick="setFilter('unread')" id="unreadTab"> <i class="fas fa-envelope"></i> {{ t('ui.common.unread', {}, locale, ['common']) }} <span class="badge bg-primary ms-1" id="unreadCount">0</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#" onclick="setFilter('read')" id="readTab"> <i class="far fa-envelope-open"></i> {{ t('ui.common.read', {}, locale, ['common']) }} <span class="badge bg-success ms-1" id="readCount">0</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#" onclick="setFilter('tome')" id="toMeTab"> <i class="fas fa-user"></i> {{ t('ui.echomail.to_me', {}, locale, ['common']) }} <span class="badge bg-info ms-1" id="toMeCount">0</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#" onclick="setFilter('saved')" id="savedTab"> <i class="fas fa-bookmark"></i> {{ t('ui.echomail.saved_items', {}, locale, ['common']) }} <span class="badge bg-warning ms-1" id="savedCount">0</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="#" onclick="setFilter('drafts')" id="draftsTab"> <i class="fas fa-edit"></i> {{ t('ui.common.drafts', {}, locale, ['common']) }} <span class="badge bg-secondary ms-1" id="draftsCount">0</span> </a> </li> </ul> </div> <div class="card-body p-0"> <div id="messagesContainer"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.common.loading_messages', {}, locale, ['common']) }} </div> </div> </div> <div class="card-footer"> <nav id="pagination"> <!-- Pagination will be inserted here --> </nav> </div> </div> </div> <!-- Echo Areas Sidebar - Hidden on mobile --> <div class="col-lg-3 d-none d-lg-block"> <div class="card"> <div class="card-header p-0"> <div class="d-flex justify-content-between align-items-center px-3 pt-2 pb-1"> <h6 class="mb-0">{{ t('ui.echomail.echo_areas', {}, locale, ['common']) }}</h6> <div class="d-flex gap-2"> <a href="/echolist" class="" title="{{ t('ui.echomail.full_echo_list', {}, locale, ['common']) }}"> <i class="fa-solid fa-list"></i> </a> <a href="/subscriptions" class="" id="desktopManageLink" title="{{ t('ui.echomail.manage_subscriptions', {}, locale, ['common']) }}"> <i class="fa-solid fa-wrench"></i> </a> </div> </div> {% if interests_enabled and has_interests %} <ul class="nav nav-tabs nav-fill px-2 mb-0" id="desktopViewingTabs" role="tablist"> <li class="nav-item" role="presentation"> <button class="nav-link active py-1 small" id="desktopAreaListTab" data-bs-toggle="tab" data-bs-target="#desktopAreaListPane" type="button" role="tab"> {{ t('ui.echomail.tab_area_list', {}, locale, ['common']) }} </button> </li> <li class="nav-item" role="presentation"> <button class="nav-link py-1 small" id="desktopInterestsTab" data-bs-toggle="tab" data-bs-target="#desktopInterestsPane" type="button" role="tab"> {{ t('ui.echomail.tab_interests', {}, locale, ['common']) }} </button> </li> </ul> {% endif %} </div> <div class="card-body p-0"> {% if interests_enabled and has_interests %} <div class="tab-content"> <div class="tab-pane fade show active p-0" id="desktopAreaListPane" role="tabpanel"> <div class="px-2 pt-2"> <select class="form-select form-select-sm mb-2" id="areaListInterestFilter" onchange="setAreaListInterestFilter(this.value ? parseInt(this.value) : null)"> <option value="">{{ t('ui.echomail.all_subscribed_areas', {}, locale, ['common']) }}</option> </select> <input type="text" class="form-control form-control-sm" id="echoareaSearchInput" placeholder="{{ t('ui.echomail.search_areas_placeholder', {}, locale, ['common']) }}" onkeyup="searchEchoareas(this.value)"> </div> <div style="max-height: 268px; overflow-y: auto;"> <div id="echoareasList"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.echomail.loading_areas', {}, locale, ['common']) }} </div> </div> </div> </div> <div class="tab-pane fade p-0" id="desktopInterestsPane" role="tabpanel"> <div style="max-height: 300px; overflow-y: auto;"> <div id="desktopInterestsList"> <div class="text-center text-muted p-3"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.echomail.loading_areas', {}, locale, ['common']) }} </div> </div> </div> </div> </div> {% else %} <div class="px-2 pt-2"> <input type="text" class="form-control form-control-sm" id="echoareaSearchInput" placeholder="{{ t('ui.echomail.search_areas_placeholder', {}, locale, ['common']) }}" onkeyup="searchEchoareas(this.value)"> </div> <div style="max-height: 268px; overflow-y: auto;"> <div id="echoareasList"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.echomail.loading_areas', {}, locale, ['common']) }} </div> </div> </div> {% endif %} </div> </div> <div class="card mt-3"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.common.search_messages', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="mb-3"> <input type="text" class="form-control form-control-sm" id="searchInput" placeholder="{{ t('ui.common.search_placeholder', {}, locale, ['common']) }}"> </div> <div class="d-flex gap-2"> <button class="btn btn-outline-primary btn-sm flex-fill" onclick="searchMessages()"> <i class="fas fa-search"></i> {{ t('ui.common.search', {}, locale, ['common']) }} </button> <button class="btn btn-outline-secondary btn-sm" onclick="openAdvancedSearch()" title="{{ t('ui.common.advanced_search', {}, locale, ['common']) }}"> <i class="fas fa-sliders-h"></i> </button> </div> </div> </div> <div class="card mt-3"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.common.statistics', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <dl class="row mb-0"> <dt class="col-6">{{ t('ui.common.total', {}, locale, ['common']) }}:</dt> <dd class="col-6" id="totalMessages">-</dd> <dt class="col-6">{{ t('ui.common.unread', {}, locale, ['common']) }}:</dt> <dd class="col-6" id="unreadMessages">-</dd> <dt class="col-6">{{ t('ui.common.recent', {}, locale, ['common']) }}:</dt> <dd class="col-6" id="recentMessages">-</dd> <dt class="col-6">{{ t('ui.echomail.areas', {}, locale, ['common']) }}:</dt> <dd class="col-6" id="totalAreas">-</dd> </dl> </div> </div> </div> </div> <!-- Manage Subscriptions / Interests Button --> <div class="text-center mt-4 mb-4"> <a href="/subscriptions" id="desktopBottomManageBtn" class="btn btn-outline-primary"> <i class="fa-solid fa-wrench me-2"></i> <span id="desktopBottomManageBtnLabel">{{ t('ui.echomail.manage_subscriptions', {}, locale, ['common']) }}</span> </a> </div> <div id="messageContextMenu" class="dropdown-menu" style="display:none; position:absolute; z-index:1080;"> <button type="button" class="dropdown-item" onclick="replyFromContextMenu()"> <i class="fas fa-reply me-2"></i>{{ t('ui.common.reply', {}, 'common') }} </button> <button type="button" class="dropdown-item" onclick="repostFromContextMenu()"> <i class="fas fa-share me-2"></i>{{ t('ui.common.repost', {}, 'common') }} </button> <button type="button" class="dropdown-item" onclick="viewConversationFromContextMenu()"> <i class="fas fa-comments me-2"></i>{{ t('ui.common.view_conversation', {}, locale, ['common']) }} </button> <button type="button" class="dropdown-item" id="messageContextMenuSaveAction" onclick="toggleSaveFromContextMenu()"> <i class="fas fa-bookmark me-2"></i><span id="messageContextMenuSaveLabel">{{ t('ui.common.save_for_later', {}, locale, ['common']) }}</span> </button> <button type="button" class="dropdown-item" onclick="downloadMessageFromContextMenu()"> <i class="fas fa-download me-2"></i>{{ t('ui.common.download_message', {}, locale, ['common']) }} </button> <button type="button" class="dropdown-item" onclick="shareMessageFromContextMenu()"> <i class="fas fa-share-alt me-2"></i>{{ t('ui.common.share', {}, locale, ['common']) }} </button> <button type="button" class="dropdown-item" onclick="forwardByNetmailFromContextMenu()"> <i class="fas fa-envelope me-2"></i>{{ t('ui.common.forward_by_netmail', {}, 'common') }} </button> {% if current_user.email %} <button type="button" class="dropdown-item" onclick="forwardMessageToEmailFromContextMenu()"> <i class="fas fa-envelope me-2"></i>{{ t('ui.common.forward_to_me_by_email', {}, locale, ['common']) }} </button> {% endif %} <button type="button" class="dropdown-item" onclick="openIgnoreMessageModalFromContextMenu()"> <i class="fas fa-eye-slash me-2"></i>{{ t('ui.echomail.ignore.open_action', {}, locale, ['common']) }} </button> </div> <div class="modal fade" id="ignoreMessageModal" tabindex="-1" aria-labelledby="ignoreMessageModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <form id="ignoreMessageForm"> <div class="modal-header"> <h5 class="modal-title" id="ignoreMessageModalLabel">{{ t('ui.echomail.ignore.modal_title', {}, locale, ['common']) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, locale, ['common']) }}"></button> </div> <div class="modal-body"> <p class="text-muted small mb-3">{{ t('ui.echomail.ignore.help', {}, locale, ['common']) }}</p> <div class="mb-3"> <label for="ignoreMessageSenderDisplay" class="form-label">{{ t('ui.echomail.ignore.sender_label', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="ignoreMessageSenderDisplay" maxlength="255" readonly> <input type="hidden" id="ignoreMessageSender" name="sender_name"> <input type="hidden" id="ignoreMessageSenderAddress" name="sender_address"> </div> <div class="mb-0"> <label for="ignoreMessageSubjectContains" class="form-label">{{ t('ui.echomail.ignore.subject_contains_label', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="ignoreMessageSubjectContains" name="subject_contains" maxlength="255"> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button type="submit" class="btn btn-warning" id="ignoreMessageSubmitButton"> <i class="fas fa-eye-slash me-1"></i>{{ t('ui.echomail.ignore.save_button', {}, locale, ['common']) }} </button> </div> </form> </div> </div> </div> <!-- Message View Modal --> <div class="modal fade" id="messageModal" tabindex="-1"> <div class="modal-dialog modal-lg modal-message-reader{% if appearance.message_reader.scrollable_body %} modal-dialog-scrollable message-reader-scrollable{% endif %}"> <div class="modal-content"> <div class="modal-header"> <div class="d-flex align-items-center gap-2"> <button type="button" class="btn btn-outline-secondary btn-sm" id="prevMessageBtn" onclick="navigateMessage(-1)" title="{{ t('ui.common.previous_message', {}, locale, ['common']) }}"> <i class="fas fa-chevron-left"></i> </button> <button type="button" class="btn btn-outline-secondary btn-sm" id="nextMessageBtn" onclick="navigateMessage(1)" title="{{ t('ui.common.next_message', {}, locale, ['common']) }}"> <i class="fas fa-chevron-right"></i> </button> {% if current_user.is_admin %} <button type="button" class="btn btn-outline-secondary btn-sm" id="editMessageButton" onclick="openEditMessage()" title="{{ t('ui.common.edit', {}, locale, ['common']) }}"> <i class="fas fa-pencil-alt"></i> </button> {% endif %} <button type="button" class="btn btn-outline-secondary btn-sm" id="toggleHeaders" onclick="toggleKludgeLines()" title="{{ t('ui.common.show_kludge_lines', {}, locale, ['common']) }}"> <i class="fas fa-code" id="toggleIcon"></i> </button> {% if ai_assistant_enabled %} <button type="button" class="btn btn-outline-secondary btn-sm" id="modalAiAssistantButton" onclick="openAiAssistantFromMessageModal()" title="{{ t('ui.ai_assistant.button', {}, locale, ['common']) }}"> <i class="fas fa-robot"></i> </button> {% endif %} <button type="button" class="btn btn-outline-secondary btn-sm" id="printMessageButton" onclick="printMessage()" title="{{ t('ui.common.print', {}, locale, ['common']) }}"> <i class="fas fa-print"></i> </button> <button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleKeyboardHelp()" title="{{ t('ui.echomail.shortcuts.title', {}, locale, ['common']) }}"> <i class="fas fa-keyboard"></i> </button> </div> <h5 class="modal-title" id="messageSubject">{{ t('ui.common.message', {}, locale, ['common']) }}</h5> <button type="button" class="btn btn-outline-secondary btn-sm me-2" id="fullscreenToggle" onclick="toggleModalFullscreen()" title="{{ t('ui.common.toggle_fullscreen', {}, locale, ['common']) }}"> <i class="fas fa-expand"></i> </button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div id="messageContent"> <div class="loading-spinner"> <i class="fas fa-spinner fa-spin me-2"></i> {{ t('ui.common.loading_message', {}, locale, ['common']) }} </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary btn-compact-label" data-bs-dismiss="modal"> <i class="fas fa-times"></i> <span class="btn-text">{{ t('ui.common.close', {}, locale, ['common']) }}</span> </button> <div class="btn-group"> <button type="button" class="btn btn-outline-warning btn-compact-label" id="modalSaveButton"> <i class="fas fa-bookmark"></i> <span class="btn-text" id="modalSaveText">{{ t('ui.common.save', {}, locale, ['common']) }}</span> </button> <button type="button" class="btn btn-outline-info btn-compact-label" id="shareButton"> <i class="fas fa-share-alt"></i> <span class="btn-text">{{ t('ui.common.share', {}, locale, ['common']) }}</span> </button> <button type="button" class="btn btn-fidonet btn-compact-label" id="replyButton"> <i class="fas fa-reply"></i> <span class="btn-text">{{ t('ui.common.reply', {}, locale, ['common']) }}</span> </button> <button type="button" class="btn btn-outline-primary btn-compact-label" id="repostButton"> <i class="fas fa-share"></i> <span class="btn-text">{{ t('ui.common.repost', {}, locale, ['common']) }}</span> </button> </div> </div> {# Keyboard shortcuts help overlay ? covers the modal body area #} <div id="keyboardHelpOverlay" style="display:none; position:absolute; inset:0; z-index:20; background:rgba(0,0,0,0.82); padding:2rem; overflow-y:auto;"> <div class="mx-auto" style="max-width:480px;"> <h5 class="text-white mb-3"><i class="fas fa-keyboard me-2"></i>{{ t('ui.echomail.shortcuts.title', {}, locale, ['common']) }}</h5> <table class="table table-sm table-dark table-borderless mb-3"> <tbody> <tr> <td class="text-center" style="width:120px;"> <kbd>&larr;</kbd> <kbd>&rarr;</kbd> </td> <td>{{ t('ui.echomail.shortcuts.prev_next', {}, locale, ['common']) }}</td> </tr> <tr> <td class="text-center"><kbd>A</kbd></td> <td>{{ t('ui.echomail.shortcuts.cycle_viewer_mode', {}, locale, ['common']) }}</td> </tr> <tr> <td class="text-center"><kbd>D</kbd></td> <td>{{ t('ui.echomail.shortcuts.download', {}, locale, ['common']) }}</td> </tr> <tr> <td class="text-center"><kbd>F</kbd></td> <td>{{ t('ui.echomail.shortcuts.fullscreen', {}, locale, ['common']) }}</td> </tr> <tr> <td class="text-center"><kbd>?</kbd> <kbd>H</kbd></td> <td>{{ t('ui.echomail.shortcuts.help', {}, locale, ['common']) }}</td> </tr> <tr> <td class="text-center"><kbd>Esc</kbd></td> <td>{{ t('ui.echomail.shortcuts.close', {}, locale, ['common']) }}</td> </tr> </tbody> </table> <p class="text-muted small mb-0">{{ t('ui.echomail.shortcuts.dismiss', {}, locale, ['common']) }}</p> </div> </div> </div> </div> </div> <!-- Share Modal --> <div class="modal fade share-message-modal" id="shareModal" tabindex="-1"> <div class="modal-dialog share-message-modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">{{ t('ui.echomail.share_message', {}, locale, ['common']) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div class="alert alert-info"> <i class="fas fa-info-circle me-2"></i> {{ t('ui.echomail.share_message_help', {}, locale, ['common']) }} </div> <!-- Other users' share links, populated by JS when shares exist --> <div id="otherSharesSection" class="d-none mb-3"> <div class="alert alert-secondary py-2"> <strong><i class="fas fa-link me-1"></i>{{ t('ui.echomail.shares.existing_links', {}, locale, ['common']) }}</strong> <div id="otherSharesList" class="mt-2"></div> </div> <hr class="my-2"> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="publicShare" checked> <label class="form-check-label" for="publicShare"> <strong>{{ t('ui.echomail.allow_anonymous_access', {}, locale, ['common']) }}</strong> <br><small class="text-muted">{{ t('ui.echomail.allow_anonymous_access_help', {}, locale, ['common']) }}</small> </label> </div> <div class="mb-3" id="shareDescriptionSection"> <label for="shareDescription" class="form-label">{{ t('ui.share.description_label', {}, locale, ['common']) }}</label> <div class="input-group"> <textarea class="form-control" id="shareDescription" rows="5" placeholder="{{ t('ui.share.description_placeholder', {}, locale, ['common']) }}"></textarea> {% if ai_share_summary_enabled %} <button class="btn btn-outline-secondary" type="button" id="aiShareSummaryBtn" title="{{ t('ui.share.ai_summary_button_title', {}, locale, ['common']) }}"> <i class="fas fa-robot" id="aiShareSummaryIcon"></i> </button> {% endif %} </div> </div> <div class="mb-3" id="shareExpirySection"> <label for="shareExpiry" class="form-label">{{ t('ui.echomail.link_expires_after', {}, locale, ['common']) }}</label> <select class="form-select" id="shareExpiry"> <option value="" selected>{{ t('ui.echomail.never_expires', {}, locale, ['common']) }}</option> <option value="1">{{ t('ui.common.time.1_hour', {}, locale, ['common']) }}</option> <option value="24">{{ t('ui.common.time.24_hours', {}, locale, ['common']) }}</option> <option value="168">{{ t('ui.common.time.1_week', {}, locale, ['common']) }}</option> <option value="720">{{ t('ui.common.time.30_days', {}, locale, ['common']) }}</option> </select> </div> <div id="shareResult" class="d-none"> <label class="form-label">{{ t('ui.common.share_url', {}, locale, ['common']) }}</label> <div class="input-group mb-3"> <input type="text" class="form-control" id="shareUrl" readonly> <button class="btn btn-outline-secondary" type="button" onclick="copyShareUrl()"> <i class="fas fa-copy"></i> {{ t('ui.common.copy', {}, locale, ['common']) }} </button> </div> <div id="shareExpiryInfo" class="alert alert-info mb-3" style="display: none;"> <i class="fas fa-clock me-2"></i> <span id="shareExpiryText"></span> </div> <div id="shareAccessInfo" class="alert alert-secondary mb-3" style="display: none;"> <i class="fas fa-chart-bar me-2"></i> <span id="shareAccessText"></span> </div> <div class="alert alert-success"> <i class="fas fa-check-circle me-2"></i> {{ t('ui.echomail.share_link_created_success', {}, locale, ['common']) }} </div> <div class="mb-2"> <label class="form-label">{{ t('ui.share.og_image_label', {}, locale, ['common']) }}</label> <div class="form-text text-muted mb-2">{{ t('ui.share.og_image_help', {}, locale, ['common']) }}</div> <div id="shareOgImagePreview" class="mb-2 d-none"> <img id="shareOgImageThumb" src="" alt="Preview" class="img-thumbnail" style="max-height:120px;"> </div> <div id="shareOgImageUploadControls" class="d-flex gap-2 align-items-center flex-wrap"> <input type="file" id="shareOgImageInput" accept="image/*" class="form-control form-control-sm" style="max-width:220px;"> <button type="button" class="btn btn-sm btn-outline-secondary" id="shareOgImageUploadBtn"> <i class="fas fa-upload me-1"></i>{{ t('ui.share.og_image_upload_btn', {}, locale, ['common']) }} </button> </div> <div id="shareOgImageRemoveControls" class="d-none"> <button type="button" class="btn btn-sm btn-outline-danger" id="shareOgImageRemoveBtn"> <i class="fas fa-trash me-1"></i>{{ t('ui.share.og_image_remove_btn', {}, locale, ['common']) }} </button> </div> </div> </div> <div id="shareError" class="d-none"> <div class="alert alert-danger"> <i class="fas fa-exclamation-triangle me-2"></i> <span id="shareErrorMessage"></span> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.close', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-primary" id="createShareBtn"> <i class="fas fa-share-alt"></i> {{ t('ui.echomail.create_share_link', {}, locale, ['common']) }} </button> <button type="button" class="btn btn-outline-primary d-none" id="friendlyUrlBtn" title="Generate a readable URL based on the echo area and subject"> <i class="fas fa-link"></i> {{ t('ui.echomail.get_friendly_url', {}, locale, ['common']) }} </button> <button type="button" class="btn btn-danger d-none" id="revokeShareBtn"> <i class="fas fa-trash"></i> {{ t('ui.echomail.revoke_share', {}, locale, ['common']) }} </button> </div> </div> </div> </div> <!-- Advanced Search Modal --> <div class="modal fade" id="advancedSearchModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"><i class="fas fa-sliders-h me-2"></i>{{ t('ui.common.advanced_search.title', {}, locale, ['common']) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <p class="text-muted small mb-3">{{ t('ui.common.advanced_search.help', {}, locale, ['common']) }}</p> <div class="mb-3"> <label for="advSearchFromName" class="form-label">{{ t('ui.common.advanced_search.from_name', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="advSearchFromName" placeholder="{{ t('ui.common.advanced_search.from_name_placeholder', {}, locale, ['common']) }}"> </div> <div class="mb-3"> <label for="advSearchSubject" class="form-label">{{ t('ui.common.advanced_search.subject', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="advSearchSubject" placeholder="{{ t('ui.common.advanced_search.subject_placeholder', {}, locale, ['common']) }}"> </div> <div class="mb-3"> <label for="advSearchBody" class="form-label">{{ t('ui.common.advanced_search.body', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="advSearchBody" placeholder="{{ t('ui.common.advanced_search.body_placeholder', {}, locale, ['common']) }}"> </div> <div class="mb-3"> <label for="advSearchMessageId" class="form-label">{{ t('ui.common.advanced_search.message_id', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="advSearchMessageId" placeholder="{{ t('ui.common.advanced_search.message_id_placeholder', {}, locale, ['common']) }}"> </div> <div class="mb-3"> <label class="form-label">{{ t('ui.common.advanced_search.date_range', {}, locale, ['common']) }}</label> <div class="d-flex gap-2 align-items-center"> <input type="date" class="form-control" id="advSearchDateFrom"> <span class="text-muted flex-shrink-0">?</span> <input type="date" class="form-control" id="advSearchDateTo"> </div> </div> <div id="advSearchError" class="alert alert-danger d-none"></div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-primary" onclick="runAdvancedSearch()"> <i class="fas fa-search me-1"></i>{{ t('ui.common.search', {}, locale, ['common']) }} </button> </div> </div> </div> </div> {% if current_user.is_admin %} <!-- Edit Message Modal (admin only) --> <div class="modal fade" id="editMessageModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="editMessageModalTitle"><i class="fas fa-pencil-alt me-2"></i>{{ t('ui.echomail.edit_message', {}, locale, ['common']) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <table class="table table-sm table-borderless mb-3"> <tbody> <tr> <th class="text-muted small fw-normal ps-0" style="width:30%">{{ t('ui.common.db_id', {}, locale, ['common']) }}</th> <td class="small font-monospace" id="editMsgDbId"></td> </tr> <tr> <th class="text-muted small fw-normal ps-0">{{ t('ui.common.message_id', {}, locale, ['common']) }}</th> <td class="small font-monospace" id="editMsgId"></td> </tr> <tr> <th class="text-muted small fw-normal ps-0">{{ t('ui.common.date_label', {}, locale, ['common']) }}</th> <td class="small" id="editMsgDate"></td> </tr> <tr> <th class="text-muted small fw-normal ps-0">{{ t('ui.common.from_label', {}, locale, ['common']) }}</th> <td class="small" id="editMsgFrom"></td> </tr> <tr> <th class="text-muted small fw-normal ps-0">{{ t('ui.common.subject_label', {}, locale, ['common']) }}</th> <td class="small" id="editMsgSubject"></td> </tr> </tbody> </table> <hr> <div class="mb-3"> <label for="editArtFormat" class="form-label">{{ t('ui.echomail.art_format', {}, locale, ['common']) }}</label> <select class="form-select" id="editArtFormat"> <option value="">{{ t('ui.echomail.art_format_auto', {}, locale, ['common']) }}</option> <option value="plain">{{ t('ui.echomail.art_format_plain', {}, locale, ['common']) }}</option> <option value="ansi">{{ t('ui.echomail.art_format_ansi', {}, locale, ['common']) }}</option> <option value="amiga_ansi">{{ t('ui.echomail.art_format_amiga_ansi', {}, locale, ['common']) }}</option> </select> </div> <div class="mb-3"> <label for="editCharset" class="form-label">{{ t('ui.echomail.message_charset', {}, locale, ['common']) }}</label> <select class="form-select" id="editCharset"> {% for cs in supported_charsets %} <option value="{{ cs.value }}">{{ cs.label }}</option> {% endfor %} </select> </div> <div id="editMessageError" class="alert alert-danger d-none"></div> <div id="editMessageSuccess" class="alert alert-success d-none"> <i class="fas fa-check-circle me-2"></i>{{ t('ui.echomail.edit_message_saved', {}, locale, ['common']) }} </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.close', {}, locale, ['common']) }}</button> <button type="button" class="btn btn-primary" id="saveEditMessageBtn" onclick="saveEditMessage()"> <i class="fas fa-save me-1"></i>{{ t('ui.common.save', {}, locale, ['common']) }} </button> </div> </div> </div> </div> {% endif %} {% if ai_assistant_enabled %} {% include 'partials/ai_assistant_modal.twig' %} {% endif %} {% endblock %} {% block styles %} <style> /* Style for clickable from names in echomail */ .message-from a { color: #0d6efd !important; font-weight: 500; border-bottom: 1px dotted #0d6efd; transition: all 0.2s ease; } .message-from a:hover { color: #0a58ca !important; border-bottom: 1px solid #0a58ca; text-decoration: none !important; } /* Ensure the link stands out from the cell background */ .message-row.read .message-from a { color: #6c757d !important; } .message-row.read .message-from a:hover { color: #495057 !important; } .message-row.unread .message-from a { color: #0d6efd !important; font-weight: 600; } .message-row.unread .message-from a:hover { color: #0a58ca !important; } </style> {% endblock %} {% block scripts %} <script> // Twig-rendered variables let currentEchoarea = {{ echoarea|default(null)|json_encode|raw }}; let domain = {{ domain|default(null)|json_encode|raw }}; window.currentUserRealName = {{ current_user.real_name|json_encode|raw }}; window.isAdmin = {{ current_user.is_admin ? 'true' : 'false' }}; window.echomailDateField = '{{ echomail_date_field|default("received") }}'; </script> {% if interests_enabled %} <script src="/js/ansisys.js"></script> <script src="/js/sixel.js"></script> <script src="/js/pcboard.js"></script> <script src="/js/message-list-context-menu.js"></script> <script src="/js/file-preview.js"></script> {% endif %} <script src="/js/echomail.js"></script> <script src="/vendor/marked.min.js"></script> {% if ai_assistant_enabled %} <script src="/js/ai-assistant.js?v=1"></script> {% endif %} {% if interests_enabled and has_interests %} <script> $(document).ready(function() { function setListOrInterestPreference(value) { document.cookie = `p_listorinterest=${encodeURIComponent(value)}; path=/; max-age=31536000; SameSite=Lax`; } function getListOrInterestPreference() { const match = document.cookie.match(/(?:^|;\s*)p_listorinterest=([^;]+)/); if (!match) { return null; } const value = decodeURIComponent(match[1] || ''); return (value === 'area' || value === 'interests') ? value : null; } function getListOrInterestPreferenceForTab(tabId) { return tabId === 'mobileInterestsTab' || tabId === 'desktopInterestsTab' ? 'interests' : 'area'; } function activatePreferredTab(preference) { const mobileTabId = preference === 'interests' ? 'mobileInterestsTab' : 'mobileAreaListTab'; const desktopTabId = preference === 'interests' ? 'desktopInterestsTab' : 'desktopAreaListTab'; const mobileTab = document.getElementById(mobileTabId); const desktopTab = document.getElementById(desktopTabId); if (mobileTab) { bootstrap.Tab.getOrCreateInstance(mobileTab).show(); } if (desktopTab) { bootstrap.Tab.getOrCreateInstance(desktopTab).show(); } } // Load interests list immediately (Interests tab is active by default) $.get('/api/interests') .done(function(data) { if (data && data.interests) { const subscribedInterests = data.interests.filter(function(i) { return i.subscribed; }); displayMobileInterests(subscribedInterests); populateAreaListInterestDropdowns(subscribedInterests); // Auto-select interest from ?interest=slug URL param (e.g. returning from compose) const urlInterestSlug = new URLSearchParams(window.location.search).get('interest'); if (urlInterestSlug) { const match = data.interests.find(function(i) { return i.slug === urlInterestSlug || String(i.id) === urlInterestSlug; }); if (match) { selectInterest(match.id, match.name, match.slug); } } } }) .fail(function() { const errHtml = '<div class="text-center text-danger p-3">' + uiT('ui.interests.load_failed', 'Failed to load interests') + '</div>'; $('#mobileInterestsList').html(errHtml); $('#desktopInterestsList').html(errHtml); }); // Swap the mobile manage button label/link based on active tab function updateMobileManageBtn(tabId) { const btn = $('#mobileManageBtn'); const label = $('#mobileManageBtnLabel'); if (tabId === 'mobileInterestsTab') { btn.attr('href', '/interests'); label.text(uiT('ui.echomail.manage_interests', 'Manage Interests')); } else { btn.attr('href', '/subscriptions'); label.text(uiT('ui.echomail.manage_subscriptions', 'Manage Subscriptions')); } } // Swap the desktop manage link + bottom button based on active tab function updateDesktopManageBtn(tabId) { const link = $('#desktopManageLink'); const bottomBtn = $('#desktopBottomManageBtn'); const bottomLabel = $('#desktopBottomManageBtnLabel'); if (tabId === 'desktopInterestsTab') { link.attr('href', '/interests').attr('title', uiT('ui.echomail.manage_interests', 'Manage Interests')); bottomBtn.attr('href', '/interests'); bottomLabel.text(uiT('ui.echomail.manage_interests', 'Manage Interests')); } else { link.attr('href', '/subscriptions').attr('title', uiT('ui.echomail.manage_subscriptions', 'Manage Subscriptions')); bottomBtn.attr('href', '/subscriptions'); bottomLabel.text(uiT('ui.echomail.manage_subscriptions', 'Manage Subscriptions')); } } // Restore preferred tab, defaulting to area list unless the URL explicitly opens an interest. const initialPreference = new URLSearchParams(window.location.search).get('interest') ? 'interests' : (getListOrInterestPreference() || 'area'); activatePreferredTab(initialPreference); updateMobileManageBtn(initialPreference === 'interests' ? 'mobileInterestsTab' : 'mobileAreaListTab'); updateDesktopManageBtn(initialPreference === 'interests' ? 'desktopInterestsTab' : 'desktopAreaListTab'); setListOrInterestPreference(initialPreference); // Update on mobile tab switch $('#mobileViewingTabs button[data-bs-toggle="tab"]').on('shown.bs.tab', function() { const tabId = $(this).attr('id'); updateMobileManageBtn(tabId); setListOrInterestPreference(getListOrInterestPreferenceForTab(tabId)); }); // Update on desktop tab switch $('#desktopViewingTabs button[data-bs-toggle="tab"]').on('shown.bs.tab', function() { const tabId = $(this).attr('id'); updateDesktopManageBtn(tabId); setListOrInterestPreference(getListOrInterestPreferenceForTab(tabId)); }); }); </script> {% endif %} <style> /* Read/Unread message styling */ .message-row.unread { background-color: #f8f9fa; } /* Echoarea badge styling in subject column */ .message-subject .badge { font-size: 0.75rem; margin-bottom: 0.25rem; } .message-row.unread .message-from, .message-row.unread .message-subject { font-weight: 600; } .message-row.read { opacity: 0.85; } /* Mobile-specific improvements */ @media (max-width: 991.98px) { /* Mobile accordion styling */ #mobileEchoAreasAccordion .accordion-button { font-weight: 500; padding: 1rem; background-color: #f8f9fa; border: 2px solid #dee2e6; text-align: left; } #mobileEchoAreasAccordion .accordion-button:not(.collapsed) { background-color: #e7f3ff; border-color: #0d6efd; color: #0d6efd; box-shadow: none; } #mobileEchoAreasAccordion .accordion-button:focus { box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); border-color: #0d6efd; } /* Mobile areas list styling */ #mobileEchoareasList .list-group-item { border: none; border-bottom: 1px solid #dee2e6; padding: 0.75rem 1rem; cursor: pointer; } #mobileEchoareasList .list-group-item:hover { background-color: #f8f9fa; } #mobileEchoareasList .list-group-item.active { background-color: #0d6efd; border-color: #0d6efd; } /* Enhance message table readability on mobile */ .message-table { /*font-size: 0.9rem;*/ font-size: 1em; } .message-table th, .message-table td { padding: 0.5rem; vertical-align: middle; } } /* Small mobile devices */ @media (max-width: 575.98px) { .message-table { font-size: 0.8rem; } .badge { font-size: 0.7rem; } /* Keep all columns visible since echoarea is now integrated into subject */ /* Echoarea badge styling in message subject */ .message-subject .badge { font-size: 0.6rem; margin-bottom: 0.25rem; } } /* Sharing modal styles */ .share-message-modal-dialog { --bs-modal-width: 760px; } .modal-dialog .alert { margin-bottom: 1rem; } .share-message-modal .modal-body .form-check { border: 1px solid var(--border-color, #e9ecef); border-radius: 0.375rem; padding: 0.75rem; background-color: var(--bg-secondary, var(--message-bg, #f8f9fa)); position: relative; } .share-message-modal .modal-body .form-check:hover { background-color: var(--message-quote-bg, #e9ecef); } .share-message-modal .modal-body .form-check-input { position: relative; margin-left: 0; margin-right: 0.5rem; } .share-message-modal .modal-body .form-check-label { margin-left: 0; } .share-message-modal .modal-body .form-check-input:checked ~ .form-check-label { color: #0d6efd; } .share-message-modal .modal-body .input-group { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075); border-radius: 0.375rem; } .share-message-modal .modal-body .input-group .form-control { font-family: 'Courier New', Monaco, Consolas, monospace; font-size: 0.875rem; background-color: var(--bg-secondary, var(--message-bg, #f8f9fa)); color: var(--text-color, #212529); border: 1px solid var(--border-color, #dee2e6); } .share-message-modal .modal-body .input-group .form-control:focus { background-color: var(--bg-secondary, var(--message-bg, #f8f9fa)); color: var(--text-color, #212529); border-color: var(--fidonet-blue, #0d6efd); } .share-message-modal .modal-body .input-group .btn { border-left: none; } .modal-footer .btn-group { gap: 0.25rem; } .modal-footer .btn-group .btn { border-radius: 0.375rem !important; } #shareButton { position: relative; } #shareButton:hover { background-color: #17a2b8; border-color: #17a2b8; color: white; } /* Share result styling */ #shareResult .alert-success { border: 1px solid #d1e7dd; background-color: #d1e7dd; color: #0f5132; } #shareResult .alert-success .fas { color: #0f5132; } /* Share error styling */ #shareError .alert-danger { border: 1px solid #f5c2c7; background-color: #f8d7da; color: #842029; } #shareError .alert-danger .fas { color: #842029; } /* Loading states */ .btn .fa-spinner { animation: fa-spin 1s infinite linear; } /* Responsive modal adjustments */ @media (max-width: 576px) { .share-message-modal-dialog { --bs-modal-width: calc(100vw - 1rem); } .modal-dialog { margin: 0.5rem; } .modal-body { padding: 1rem; } .modal-body .input-group .form-control { font-size: 0.75rem; } .modal-footer { flex-direction: row; flex-wrap: wrap; gap: 0.5rem; justify-content: center; } .modal-footer .btn-group { width: auto; flex-direction: row; } .modal-footer .btn { width: auto; } .btn-compact-label { padding: 0.4rem 0.55rem; font-size: 0.85rem; } .btn-compact-label .btn-text { display: none; } } /* Threading styles with colored left borders */ .thread-root { border-left: 3px solid #0d6efd; background-color: rgba(13, 110, 253, 0.05); } .thread-reply { background-color: rgba(108, 117, 125, 0.02); } .thread-level-1 { border-left: 2px solid #6c757d; background-color: rgba(108, 117, 125, 0.05); } .thread-level-2 { border-left: 2px solid #28a745; background-color: rgba(40, 167, 69, 0.05); } .thread-level-3 { border-left: 2px solid #ffc107; background-color: rgba(255, 193, 7, 0.05); } .thread-level-4, .thread-level-5, .thread-level-6, .thread-level-7, .thread-level-8, .thread-level-9 { border-left: 2px solid #dc3545; background-color: rgba(220, 53, 69, 0.05); } /* Thread root styling enhancement */ .thread-root .message-subject { font-weight: 500; } /* Reply badge styling */ .thread-root .badge.bg-secondary { background-color: #0d6efd !important; font-size: 0.7em; } /* Hover effects for threads */ .thread-root:hover { background-color: rgba(13, 110, 253, 0.1) !important; } .thread-reply:hover { background-color: rgba(108, 117, 125, 0.1) !important; } /* Reply indentation handled inline per thread level */ /* Mobile threading adjustments */ @media (max-width: 991.98px) { .thread-level-1, .thread-level-2, .thread-level-3, .thread-level-4, .thread-level-5, .thread-level-6, .thread-level-7, .thread-level-8, .thread-level-9 { border-left-width: 1px; } .thread-root { border-left-width: 2px; } } @media (max-width: 575.98px) { } /* Green text for messages addressed to current user */ .text-green { color: #198754 !important; } /* Search term highlighting */ .search-highlight { background-color: #ffff00 !important; color: #000000 !important; font-weight: 600; padding: 0.1em 0.2em; border-radius: 0.2em; box-shadow: 0 0 0 1px #cccc00; } /* Ensure highlighting works in different contexts */ .message-formatted .search-highlight, .message-preformatted .search-highlight, .quote-line .search-highlight, .message-line .search-highlight, .message-signature .search-highlight { background-color: #ffff00 !important; color: #000000 !important; font-weight: 600; padding: 0.1em 0.2em; border-radius: 0.2em; box-shadow: 0 0 0 1px #cccc00; } /* Dark mode support for search highlighting */ @media (prefers-color-scheme: dark) { .search-highlight, .message-formatted .search-highlight, .message-preformatted .search-highlight, .quote-line .search-highlight, .message-line .search-highlight, .message-signature .search-highlight { background-color: #ffff66 !important; color: #000000 !important; box-shadow: 0 0 0 1px #cccc00; } } /* Threading icon styling */ .message-from .fa-reply { color: #6c757d; font-size: 0.85em; margin-right: 0.25rem; } /* Enhanced threading icon visibility */ .thread-reply .message-from .fa-reply { color: #0d6efd; } /* Message modal navigation buttons */ .modal-header .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.875rem; min-width: 32px; } .modal-header .btn-sm:disabled { opacity: 0.3; cursor: not-allowed; } .modal-header .btn-sm i { font-size: 0.75rem; } /* Modal header layout improvements */ .modal-header { display: flex; justify-content: space-between; align-items: center; } .modal-header .d-flex { flex-shrink: 0; } .modal-title { flex: 1; text-align: center; margin: 0 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Mobile modal navigation adjustments */ @media (max-width: 576px) { .modal-header .btn-sm { padding: 0.2rem 0.4rem; font-size: 0.8rem; min-width: 28px; } .modal-header .btn-sm i { font-size: 0.7rem; } .modal-title { font-size: 1rem; margin: 0 0.5rem; } } /* Touch/swipe improvements */ .modal-dialog { touch-action: pan-y; /* Allow vertical scrolling but enable horizontal swipe detection */ } .modal-content { overflow: hidden; /* Prevent content from extending beyond modal bounds during swipes */ } /* Add a subtle hint for swipe functionality on touch devices */ @media (pointer: coarse) { .modal-header::after { content: "? Swipe to navigate ?"; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); font-size: 0.7rem; color: #6c757d; opacity: 0.7; white-space: nowrap; pointer-events: none; z-index: 1; background: rgba(255, 255, 255, 0.9); padding: 0.25rem 0.5rem; border-radius: 0.25rem; animation: fadeInOut 4s ease-in-out; } } @keyframes fadeInOut { 0%, 15%, 85%, 100% { opacity: 0; } 30%, 70% { opacity: 0.7; } } </style> {% endblock %}