mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add extra info tooltips to Spinner.
This commit is contained in:
committed by
jeffrey-signal
parent
f2582cae54
commit
952990c8af
@@ -8,8 +8,13 @@ package org.thoughtcrime.securesms
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.spinner.Plugin
|
||||
import org.signal.spinner.PluginResult
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
|
||||
class ApiPlugin : Plugin {
|
||||
@@ -22,7 +27,10 @@ class ApiPlugin : Plugin {
|
||||
override val path: String = PATH
|
||||
|
||||
private val apis = mapOf(
|
||||
"localBackups" to ::localBackups
|
||||
"localBackups" to ::localBackups,
|
||||
"recipient" to ::recipient,
|
||||
"thread" to ::thread,
|
||||
"message" to ::message
|
||||
)
|
||||
|
||||
override fun get(parameters: Map<String, List<String>>): PluginResult {
|
||||
@@ -32,7 +40,7 @@ class ApiPlugin : Plugin {
|
||||
val apiButtons = apis.keys.joinToString("\n") { apiName ->
|
||||
"""
|
||||
<div style="margin-bottom: 20px;">
|
||||
<button id="btn_$apiName" style="padding: 10px; margin-right: 10px;">Call $PATH?api=$apiName</button>
|
||||
<button id="btn_$apiName" style="padding: 10px; margin-right: 10px;">Call $PATH?api=$apiName&id=1</button>
|
||||
<span id="result_$apiName" style="font-weight: bold;"></span>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
@@ -45,7 +53,7 @@ class ApiPlugin : Plugin {
|
||||
resultSpan.textContent = 'Loading...';
|
||||
|
||||
try {
|
||||
const response = await fetch('$PATH?api=$apiName');
|
||||
const response = await fetch('$PATH?api=$apiName&id=1');
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
resultSpan.textContent = 'Error: ' + errorText;
|
||||
@@ -98,7 +106,137 @@ class ApiPlugin : Plugin {
|
||||
return PluginResult.JsonResult(JsonUtil.toJson(PluginCache.localBackups))
|
||||
}
|
||||
|
||||
private fun recipient(parameters: Map<String, List<String>>): PluginResult {
|
||||
val recipientId = parameters["id"]?.firstOrNull()?.toLongOrNull() ?: return PluginResult.ErrorResult.notFound("recipient not found")
|
||||
|
||||
val recipient = Recipient.resolved(RecipientId.from(recipientId))
|
||||
|
||||
val groupType = when {
|
||||
recipient.isMmsGroup -> "MMS"
|
||||
recipient.isPushV2Group -> "Signal V2"
|
||||
recipient.isPushV1Group -> "Signal V1"
|
||||
recipient.isPushGroup -> "Signal"
|
||||
else -> null
|
||||
}
|
||||
|
||||
return RecipientInfo(
|
||||
name = recipient.getDisplayName(AppDependencies.application),
|
||||
aci = recipient.aci.map { it.toString() }.orNull(),
|
||||
pni = recipient.pni.map { it.toString() }.orNull(),
|
||||
e164 = recipient.e164.orNull(),
|
||||
username = recipient.username.orNull(),
|
||||
nickname = recipient.nickname.toString().takeIf { it.isNotBlank() },
|
||||
note = recipient.note,
|
||||
about = recipient.about,
|
||||
aboutEmoji = recipient.aboutEmoji,
|
||||
isBlocked = recipient.isBlocked,
|
||||
isSystemContact = recipient.isSystemContact,
|
||||
isGroup = recipient.isGroup,
|
||||
groupId = recipient.groupId.map { it.toString() }.orNull(),
|
||||
groupType = groupType,
|
||||
isActiveGroup = recipient.isActiveGroup,
|
||||
participantCount = if (recipient.isGroup) recipient.participantIds.size else null
|
||||
).toJsonResult()
|
||||
}
|
||||
|
||||
private fun thread(parameters: Map<String, List<String>>): PluginResult {
|
||||
val threadId = parameters["id"]?.firstOrNull()?.toLongOrNull() ?: return PluginResult.ErrorResult.notFound("thread not found")
|
||||
|
||||
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId) ?: return PluginResult.ErrorResult(message = "Thread not found")
|
||||
|
||||
return ThreadInfo(
|
||||
threadId = threadRecord.threadId,
|
||||
recipientName = threadRecord.recipient.getDisplayName(AppDependencies.application),
|
||||
recipientId = threadRecord.recipient.id.toLong(),
|
||||
unreadCount = threadRecord.unreadCount,
|
||||
snippet = threadRecord.body,
|
||||
date = threadRecord.date,
|
||||
archived = threadRecord.isArchived,
|
||||
pinned = threadRecord.isPinned,
|
||||
unreadSelfMentionsCount = threadRecord.unreadSelfMentionsCount
|
||||
).toJsonResult()
|
||||
}
|
||||
|
||||
private fun message(parameters: Map<String, List<String>>): PluginResult {
|
||||
val messageId = parameters["id"]?.firstOrNull()?.toLongOrNull() ?: return PluginResult.ErrorResult.notFound("message not found")
|
||||
|
||||
val messageRecord = try {
|
||||
SignalDatabase.messages.getMessageRecord(messageId)
|
||||
} catch (e: Exception) {
|
||||
return PluginResult.ErrorResult(message = "Message not found: ${e.message}")
|
||||
}
|
||||
|
||||
val messageType = when {
|
||||
messageRecord.isUpdate -> "Update"
|
||||
messageRecord.isOutgoing -> "Outgoing"
|
||||
else -> "Incoming"
|
||||
}
|
||||
|
||||
return MessageInfo(
|
||||
messageId = messageRecord.id,
|
||||
threadId = messageRecord.threadId,
|
||||
body = messageRecord.body,
|
||||
fromRecipientName = messageRecord.fromRecipient.getDisplayName(AppDependencies.application),
|
||||
fromRecipientId = messageRecord.fromRecipient.id.toLong(),
|
||||
toRecipientName = messageRecord.toRecipient.getDisplayName(AppDependencies.application),
|
||||
toRecipientId = messageRecord.toRecipient.id.toLong(),
|
||||
dateSent = messageRecord.dateSent,
|
||||
dateReceived = messageRecord.dateReceived,
|
||||
isOutgoing = messageRecord.isOutgoing,
|
||||
type = messageType
|
||||
).toJsonResult()
|
||||
}
|
||||
|
||||
data class LocalBackups @JsonCreator constructor(@field:JsonProperty val backups: List<LocalBackup>)
|
||||
|
||||
data class LocalBackup @JsonCreator constructor(@field:JsonProperty val name: String, @field:JsonProperty val timestamp: Long)
|
||||
|
||||
data class RecipientInfo @JsonCreator constructor(
|
||||
@field:JsonProperty val name: String,
|
||||
@field:JsonProperty val aci: String? = null,
|
||||
@field:JsonProperty val pni: String? = null,
|
||||
@field:JsonProperty val e164: String? = null,
|
||||
@field:JsonProperty val username: String? = null,
|
||||
@field:JsonProperty val nickname: String? = null,
|
||||
@field:JsonProperty val note: String? = null,
|
||||
@field:JsonProperty val about: String? = null,
|
||||
@field:JsonProperty val aboutEmoji: String? = null,
|
||||
@field:JsonProperty val isBlocked: Boolean = false,
|
||||
@field:JsonProperty val isSystemContact: Boolean = false,
|
||||
@field:JsonProperty val isGroup: Boolean = false,
|
||||
@field:JsonProperty val groupId: String? = null,
|
||||
@field:JsonProperty val groupType: String? = null,
|
||||
@field:JsonProperty val isActiveGroup: Boolean = false,
|
||||
@field:JsonProperty val participantCount: Int? = null
|
||||
)
|
||||
|
||||
data class ThreadInfo @JsonCreator constructor(
|
||||
@field:JsonProperty val threadId: Long,
|
||||
@field:JsonProperty val recipientName: String,
|
||||
@field:JsonProperty val recipientId: Long,
|
||||
@field:JsonProperty val unreadCount: Int,
|
||||
@field:JsonProperty val snippet: String? = null,
|
||||
@field:JsonProperty val date: Long,
|
||||
@field:JsonProperty val archived: Boolean = false,
|
||||
@field:JsonProperty val pinned: Boolean = false,
|
||||
@field:JsonProperty val unreadSelfMentionsCount: Int = 0
|
||||
)
|
||||
|
||||
data class MessageInfo @JsonCreator constructor(
|
||||
@field:JsonProperty val messageId: Long,
|
||||
@field:JsonProperty val threadId: Long,
|
||||
@field:JsonProperty val body: String? = null,
|
||||
@field:JsonProperty val fromRecipientName: String,
|
||||
@field:JsonProperty val fromRecipientId: Long,
|
||||
@field:JsonProperty val toRecipientName: String? = null,
|
||||
@field:JsonProperty val toRecipientId: Long? = null,
|
||||
@field:JsonProperty val dateSent: Long,
|
||||
@field:JsonProperty val dateReceived: Long,
|
||||
@field:JsonProperty val isOutgoing: Boolean,
|
||||
@field:JsonProperty val type: String
|
||||
)
|
||||
|
||||
fun Any.toJsonResult(): PluginResult.JsonResult {
|
||||
return PluginResult.JsonResult(JsonUtil.toJson(this))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.AttachmentTransformer
|
||||
import org.thoughtcrime.securesms.database.DatabaseMonitor
|
||||
import org.thoughtcrime.securesms.database.GV2Transformer
|
||||
import org.thoughtcrime.securesms.database.GV2UpdateTransformer
|
||||
import org.thoughtcrime.securesms.database.IdPopupTransformer
|
||||
import org.thoughtcrime.securesms.database.IsStoryTransformer
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
@@ -72,7 +73,8 @@ class SpinnerApplicationContext : ApplicationContext() {
|
||||
KyberKeyTransformer,
|
||||
RecipientTransformer,
|
||||
AttachmentTransformer,
|
||||
PollTransformer
|
||||
PollTransformer,
|
||||
IdPopupTransformer
|
||||
)
|
||||
),
|
||||
"jobmanager" to DatabaseConfig(db = { JobDatabase.getInstance(this).sqlCipherDatabase }, columnTransformers = listOf(TimestampTransformer)),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.signal.spinner.DefaultColumnTransformer
|
||||
|
||||
object IdPopupTransformer : ColumnTransformer {
|
||||
|
||||
private val recipientColumns = setOf(
|
||||
MessageTable.FROM_RECIPIENT_ID,
|
||||
MessageTable.TO_RECIPIENT_ID,
|
||||
ThreadTable.RECIPIENT_ID
|
||||
)
|
||||
|
||||
private val threadIdColumns = setOf(
|
||||
MessageTable.THREAD_ID
|
||||
)
|
||||
|
||||
private val messageIdColumns = setOf(
|
||||
AttachmentTable.MESSAGE_ID,
|
||||
ThreadTable.SNIPPET_MESSAGE_ID
|
||||
)
|
||||
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
return recipientColumns.contains(columnName) || threadIdColumns.contains(columnName) || messageIdColumns.contains(columnName)
|
||||
}
|
||||
|
||||
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String? {
|
||||
val default = DefaultColumnTransformer.transform(tableName, columnName, cursor)
|
||||
|
||||
return if (recipientColumns.contains(columnName)) {
|
||||
default?.let {
|
||||
"""<div class="popup" data-recipient-id="$it">$it<span class="tooltip">Loading...</span></div>"""
|
||||
}
|
||||
} else if (threadIdColumns.contains(columnName)) {
|
||||
default?.let {
|
||||
"""<div class="popup" data-thread-id="$it">$it<span class="tooltip">Loading...</span></div>"""
|
||||
}
|
||||
} else if (messageIdColumns.contains(columnName)) {
|
||||
default?.let {
|
||||
"""<div class="popup" data-message-id="$it">$it<span class="tooltip">Loading...</span></div>"""
|
||||
}
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,5 +71,11 @@
|
||||
</form>
|
||||
|
||||
{{> partials/suffix}}
|
||||
|
||||
{{> partials/tooltips}}
|
||||
|
||||
<script type="text/javascript">
|
||||
initializeTooltips();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
73
spinner/lib/src/main/assets/css/tooltips.css
Normal file
73
spinner/lib/src/main/assets/css/tooltips.css
Normal file
@@ -0,0 +1,73 @@
|
||||
td {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
td pre {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: relative;
|
||||
cursor: help;
|
||||
text-decoration: underline dotted;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.popup .tooltip {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 200%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: pre-wrap;
|
||||
max-width: 600px;
|
||||
min-width: 200px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.popup .tooltip.align-left {
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.popup .tooltip.align-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.popup .tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #555 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.popup .tooltip.align-left::after {
|
||||
left: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.popup .tooltip.align-right::after {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.popup:hover .tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
324
spinner/lib/src/main/assets/js/tooltips.js
Normal file
324
spinner/lib/src/main/assets/js/tooltips.js
Normal file
@@ -0,0 +1,324 @@
|
||||
function adjustTooltipPosition(tooltipSpan) {
|
||||
// Reset any previous alignment classes
|
||||
tooltipSpan.classList.remove('align-left', 'align-right');
|
||||
|
||||
// Get tooltip and viewport dimensions
|
||||
const tooltipRect = tooltipSpan.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
// Check if tooltip overflows on the left
|
||||
if (tooltipRect.left < 0) {
|
||||
tooltipSpan.classList.add('align-left');
|
||||
}
|
||||
// Check if tooltip overflows on the right
|
||||
else if (tooltipRect.right > viewportWidth) {
|
||||
tooltipSpan.classList.add('align-right');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeRecipientTooltips() {
|
||||
const popupElements = document.querySelectorAll('.popup[data-recipient-id]');
|
||||
const cache = {};
|
||||
|
||||
popupElements.forEach(element => {
|
||||
const recipientId = element.getAttribute('data-recipient-id');
|
||||
const tooltipSpan = element.querySelector('.tooltip');
|
||||
|
||||
if (!recipientId || !tooltipSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasLoaded = false;
|
||||
|
||||
element.addEventListener('mouseenter', async function() {
|
||||
if (hasLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache[recipientId]) {
|
||||
tooltipSpan.textContent = cache[recipientId];
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api?api=recipient&id=${recipientId}`);
|
||||
if (!response.ok) {
|
||||
tooltipSpan.textContent = `Error loading recipient`;
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const lines = [];
|
||||
|
||||
// Name (primary)
|
||||
let nameLine = data.isGroup ? `Group: ${data.name || recipientId}` : `Name: ${data.name || recipientId}`;
|
||||
if (data.nickname) {
|
||||
nameLine += ` (${data.nickname})`;
|
||||
}
|
||||
lines.push(nameLine);
|
||||
|
||||
// Group-specific information
|
||||
if (data.isGroup) {
|
||||
if (data.groupType) {
|
||||
lines.push(`Type: ${data.groupType}`);
|
||||
}
|
||||
if (data.groupId) {
|
||||
lines.push(`Group ID: ${data.groupId}`);
|
||||
}
|
||||
if (data.participantCount !== null && data.participantCount !== undefined) {
|
||||
const memberText = data.participantCount === 1 ? 'member' : 'members';
|
||||
lines.push(`Members: ${data.participantCount} ${memberText}`);
|
||||
}
|
||||
const groupStatus = data.isActiveGroup ? 'Active' : 'Inactive';
|
||||
lines.push(`Status: ${groupStatus}`);
|
||||
} else {
|
||||
// Individual-specific information
|
||||
// ACI
|
||||
if (data.aci) {
|
||||
lines.push(`ACI: ${data.aci}`);
|
||||
}
|
||||
|
||||
// PNI
|
||||
if (data.pni) {
|
||||
lines.push(`PNI: ${data.pni}`);
|
||||
}
|
||||
|
||||
// Username
|
||||
if (data.username) {
|
||||
lines.push(`Username: @${data.username}`);
|
||||
}
|
||||
|
||||
// Phone number
|
||||
if (data.e164) {
|
||||
lines.push(`Phone: ${data.e164}`);
|
||||
}
|
||||
|
||||
// About/Bio
|
||||
if (data.about) {
|
||||
const aboutText = data.aboutEmoji ? `${data.aboutEmoji} ${data.about}` : data.about;
|
||||
lines.push(`About: ${aboutText}`);
|
||||
}
|
||||
|
||||
// Note
|
||||
if (data.note) {
|
||||
lines.push(`Note: 📝 ${data.note}`);
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
const statusParts = [];
|
||||
if (data.isBlocked) {
|
||||
statusParts.push('🚫 Blocked');
|
||||
}
|
||||
if (data.isSystemContact) {
|
||||
statusParts.push('📱 Contact');
|
||||
}
|
||||
if (statusParts.length > 0) {
|
||||
lines.push(`Status: ${statusParts.join(' | ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const displayText = lines.join('\n');
|
||||
tooltipSpan.textContent = displayText;
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
cache[recipientId] = displayText;
|
||||
hasLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('Error fetching recipient data:', error);
|
||||
tooltipSpan.textContent = `Error: ${error.message}`;
|
||||
hasLoaded = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeThreadTooltips() {
|
||||
const popupElements = document.querySelectorAll('.popup[data-thread-id]');
|
||||
const cache = {};
|
||||
|
||||
popupElements.forEach(element => {
|
||||
const threadId = element.getAttribute('data-thread-id');
|
||||
const tooltipSpan = element.querySelector('.tooltip');
|
||||
|
||||
if (!threadId || !tooltipSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasLoaded = false;
|
||||
|
||||
element.addEventListener('mouseenter', async function() {
|
||||
if (hasLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache[threadId]) {
|
||||
tooltipSpan.textContent = cache[threadId];
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api?api=thread&id=${threadId}`);
|
||||
if (!response.ok) {
|
||||
tooltipSpan.textContent = `Error loading thread`;
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const lines = [];
|
||||
|
||||
// Thread ID
|
||||
lines.push(`Thread ID: ${data.threadId}`);
|
||||
|
||||
// Recipient
|
||||
lines.push(`Recipient: ${data.recipientName} (ID: ${data.recipientId})`);
|
||||
|
||||
// Unread count
|
||||
if (data.unreadCount > 0) {
|
||||
lines.push(`Unread: ${data.unreadCount}`);
|
||||
}
|
||||
|
||||
// Unread self mentions
|
||||
if (data.unreadSelfMentionsCount > 0) {
|
||||
lines.push(`Unread Mentions: ${data.unreadSelfMentionsCount}`);
|
||||
}
|
||||
|
||||
// Snippet
|
||||
if (data.snippet) {
|
||||
const snippetPreview = data.snippet.length > 50
|
||||
? data.snippet.substring(0, 50) + '...'
|
||||
: data.snippet;
|
||||
lines.push(`Snippet: ${snippetPreview}`);
|
||||
}
|
||||
|
||||
// Date
|
||||
if (data.date) {
|
||||
const date = new Date(data.date);
|
||||
lines.push(`Last Activity: ${date.toLocaleString()}`);
|
||||
}
|
||||
|
||||
// Status flags
|
||||
const statusParts = [];
|
||||
if (data.pinned) {
|
||||
statusParts.push('📌 Pinned');
|
||||
}
|
||||
if (data.archived) {
|
||||
statusParts.push('📦 Archived');
|
||||
}
|
||||
if (statusParts.length > 0) {
|
||||
lines.push(`Status: ${statusParts.join(' | ')}`);
|
||||
}
|
||||
|
||||
const displayText = lines.join('\n');
|
||||
tooltipSpan.textContent = displayText;
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
cache[threadId] = displayText;
|
||||
hasLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('Error fetching thread data:', error);
|
||||
tooltipSpan.textContent = `Error: ${error.message}`;
|
||||
hasLoaded = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeMessageTooltips() {
|
||||
const popupElements = document.querySelectorAll('.popup[data-message-id]');
|
||||
const cache = {};
|
||||
|
||||
popupElements.forEach(element => {
|
||||
const messageId = element.getAttribute('data-message-id');
|
||||
const tooltipSpan = element.querySelector('.tooltip');
|
||||
|
||||
if (!messageId || !tooltipSpan) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasLoaded = false;
|
||||
|
||||
element.addEventListener('mouseenter', async function() {
|
||||
if (hasLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache[messageId]) {
|
||||
tooltipSpan.textContent = cache[messageId];
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api?api=message&id=${messageId}`);
|
||||
if (!response.ok) {
|
||||
tooltipSpan.textContent = `Error loading message`;
|
||||
hasLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const lines = [];
|
||||
|
||||
// Message ID
|
||||
lines.push(`Message ID: ${data.messageId}`);
|
||||
|
||||
// Type
|
||||
lines.push(`Type: ${data.type}`);
|
||||
|
||||
// From
|
||||
lines.push(`From: ${data.fromRecipientName} (ID: ${data.fromRecipientId})`);
|
||||
|
||||
// To
|
||||
if (data.toRecipientName) {
|
||||
lines.push(`To: ${data.toRecipientName} (ID: ${data.toRecipientId})`);
|
||||
}
|
||||
|
||||
// Thread ID
|
||||
lines.push(`Thread ID: ${data.threadId}`);
|
||||
|
||||
// Body
|
||||
if (data.body) {
|
||||
const bodyPreview = data.body.length > 50
|
||||
? data.body.substring(0, 50) + '...'
|
||||
: data.body;
|
||||
lines.push(`Body: ${bodyPreview}`);
|
||||
}
|
||||
|
||||
// Dates
|
||||
if (data.dateSent) {
|
||||
const dateSent = new Date(data.dateSent);
|
||||
lines.push(`Sent: ${dateSent.toLocaleString()}`);
|
||||
}
|
||||
|
||||
if (data.dateReceived) {
|
||||
const dateReceived = new Date(data.dateReceived);
|
||||
lines.push(`Received: ${dateReceived.toLocaleString()}`);
|
||||
}
|
||||
|
||||
const displayText = lines.join('\n');
|
||||
tooltipSpan.textContent = displayText;
|
||||
adjustTooltipPosition(tooltipSpan);
|
||||
cache[messageId] = displayText;
|
||||
hasLoaded = true;
|
||||
} catch (error) {
|
||||
console.error('Error fetching message data:', error);
|
||||
tooltipSpan.textContent = `Error: ${error.message}`;
|
||||
hasLoaded = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeTooltips() {
|
||||
initializeRecipientTooltips();
|
||||
initializeThreadTooltips();
|
||||
initializeMessageTooltips();
|
||||
}
|
||||
2
spinner/lib/src/main/assets/partials/tooltips.hbs
Normal file
2
spinner/lib/src/main/assets/partials/tooltips.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
<link rel="stylesheet" href="/css/tooltips.css">
|
||||
<script src="/js/tooltips.js"></script>
|
||||
@@ -49,6 +49,8 @@
|
||||
|
||||
{{> partials/suffix}}
|
||||
|
||||
{{> partials/tooltips}}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql-formatter/4.0.2/sql-formatter.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/ace.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.5/mode-sql.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
@@ -61,6 +63,7 @@
|
||||
//document.querySelector('.query-input').addEventListener('keypress', submitOnEnter);
|
||||
document.getElementById('query-form').addEventListener('submit', onQuerySubmitted, false);
|
||||
renderQueryHistory();
|
||||
initializeTooltips();
|
||||
|
||||
editor = ace.edit(document.querySelector('.query-input'), {
|
||||
mode: 'ace/mode/sql',
|
||||
|
||||
Reference in New Issue
Block a user