This commit is contained in:
KnugiHK
2025-07-27 16:25:47 +08:00
parent 49851f5874
commit 101e554413
+464 -355
View File
@@ -1,46 +1,54 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<title>Whatsapp - {{ name }}</title> <head>
<meta charset="UTF-8"> <title>Whatsapp - {{ name }}</title>
<script src="https://cdn.tailwindcss.com"></script> <meta charset="UTF-8">
<script> <script src="https://cdn.tailwindcss.com"></script>
tailwind.config = { <script>
theme: { tailwind.config = {
extend: { theme: {
colors: { extend: {
whatsapp: { colors: {
light: '#e7ffdb', whatsapp: {
DEFAULT: '#25D366', light: '#e7ffdb',
dark: '#075E54', DEFAULT: '#25D366',
chat: '#efeae2', dark: '#075E54',
'chat-light': '#f0f2f5', chat: '#efeae2',
} 'chat-light': '#f0f2f5',
} }
} }
} }
} }
</script> }
<style> </script>
body, html { <style>
body,
html {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
scroll-behavior: smooth !important; scroll-behavior: smooth !important;
} }
.chat-list { .chat-list {
height: calc(100vh - 120px); height: calc(100vh - 120px);
overflow-y: auto; overflow-y: auto;
} }
.message-list { .message-list {
height: calc(100vh - 90px); height: calc(100vh - 90px);
overflow-y: auto; overflow-y: auto;
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.chat-list, .message-list {
.chat-list,
.message-list {
height: calc(100vh - 108px); height: calc(100vh - 108px);
} }
} }
header { header {
position: fixed; position: fixed;
z-index: 20; z-index: 20;
@@ -50,26 +58,33 @@
background-color: white; background-color: white;
padding: 20px 0 20px 0; padding: 20px 0 20px 0;
} }
footer { footer {
margin-top: 10px; margin-top: 10px;
border-top: 2px solid #e3e6e7; border-top: 2px solid #e3e6e7;
padding: 20px 0 20px 0; padding: 20px 0 20px 0;
} }
article { article {
width:430px; width: 430px;
margin: auto; margin: auto;
z-index:10; z-index: 10;
font-size: 15px; font-size: 15px;
word-wrap: break-word; word-wrap: break-word;
} }
img, video, audio{
max-width:100%; img,
video,
audio {
max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
div.reply{
div.reply {
font-size: 13px; font-size: 13px;
text-decoration: none; text-decoration: none;
} }
div:target::before { div:target::before {
content: ''; content: '';
display: block; display: block;
@@ -77,73 +92,91 @@
margin-top: -115px; margin-top: -115px;
visibility: hidden; visibility: hidden;
} }
div:target { div:target {
animation: 3s highlight; animation: 3s highlight;
} }
.avatar { .avatar {
border-radius:50%; border-radius: 50%;
overflow:hidden; overflow: hidden;
max-width: 64px; max-width: 64px;
max-height: 64px; max-height: 64px;
} }
.name { .name {
color: #3892da; color: #3892da;
} }
.pad-left-10 { .pad-left-10 {
padding-left: 10px; padding-left: 10px;
} }
.pad-right-10 { .pad-right-10 {
padding-right: 10px; padding-right: 10px;
} }
.reply_link { .reply_link {
color: #168acc; color: #168acc;
} }
.blue { .blue {
color: #70777a; color: #70777a;
} }
.sticker { .sticker {
max-width: 100px !important; max-width: 100px !important;
max-height: 100px !important; max-height: 100px !important;
} }
@keyframes highlight { @keyframes highlight {
from { from {
background-color: rgba(37, 211, 102, 0.1); background-color: rgba(37, 211, 102, 0.1);
} }
to { to {
background-color: transparent; background-color: transparent;
} }
} }
.search-input { .search-input {
transform: translateY(-100%); transform: translateY(-100%);
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
} }
.search-input.active { .search-input.active {
transform: translateY(0); transform: translateY(0);
} }
.reply-box:active { .reply-box:active {
background-color:rgb(200 202 205 / var(--tw-bg-opacity, 1)); background-color: rgb(200 202 205 / var(--tw-bg-opacity, 1));
} }
.info-box-tooltip { .info-box-tooltip {
--tw-translate-x: -50%; --tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
.status-indicator { .status-indicator {
display: inline-block; display: inline-block;
margin-left: 4px; margin-left: 4px;
font-size: 0.8em; font-size: 0.8em;
color: #8c8c8c; color: #8c8c8c;
} }
.status-indicator.read { .status-indicator.read {
color: #34B7F1; color: #34B7F1;
} }
.play-icon { .play-icon {
width: 0; width: 0;
height: 0; height: 0;
border-left: 8px solid white; border-left: 8px solid white;
border-top: 5px solid transparent; border-top: 5px solid transparent;
border-bottom: 5px solid transparent; border-bottom: 5px solid transparent;
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.3)); filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
} }
.speaker-icon { .speaker-icon {
position: relative; position: relative;
width: 8px; width: 8px;
@@ -151,6 +184,7 @@
background: #666; background: #666;
border-radius: 1px 0 0 1px; border-radius: 1px 0 0 1px;
} }
.speaker-icon::before { .speaker-icon::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -162,6 +196,7 @@
border-top: 4px solid transparent; border-top: 4px solid transparent;
border-bottom: 4px solid transparent; border-bottom: 4px solid transparent;
} }
.speaker-icon::after { .speaker-icon::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -173,296 +208,371 @@
border-left: none; border-left: none;
border-radius: 0 8px 8px 0; border-radius: 0 8px 8px 0;
} }
</style>
<script> .search-icon {
function search(event) { width: 20px;
keywords = document.getElementById("mainHeaderSearchInput").value; height: 20px;
hits = []; position: relative;
document.querySelectorAll(".message-text").forEach(elem => { display: inline-block;
if (elem.innerText.trim().includes(keywords)){ }
hits.push(elem.parentElement.parentElement.id);
} .search-icon::before {
}) content: '';
console.log(hits); position: absolute;
} width: 12px;
</script> height: 12px;
border: 2px solid #aebac1;
border-radius: 50%;
top: 2px;
left: 2px;
}
.search-icon::after {
content: '';
position: absolute;
width: 2px;
height: 6px;
background: #aebac1;
transform: rotate(45deg);
top: 12px;
left: 12px;
}
.arrow-left {
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 8px solid #aebac1;
display: inline-block;
}
.arrow-right {
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 8px solid #aebac1;
display: inline-block;
}
.info-icon {
width: 20px;
height: 20px;
border: 2px solid currentColor;
border-radius: 50%;
position: relative;
display: inline-block;
}
.info-icon::before {
content: 'i';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
font-style: normal;
}
</style>
<script>
function search(event) {
keywords = document.getElementById("mainHeaderSearchInput").value;
hits = [];
document.querySelectorAll(".message-text").forEach(elem => {
if (elem.innerText.trim().includes(keywords)) {
hits.push(elem.parentElement.parentElement.id);
}
})
console.log(hits);
}
</script>
<base href="{{ media_base }}" target="_blank"> <base href="{{ media_base }}" target="_blank">
</head> </head>
<body>
<article class="h-screen bg-whatsapp-chat-light"> <body>
<div class="w-full flex flex-col"> <article class="h-screen bg-whatsapp-chat-light">
<div class="p-3 bg-whatsapp-dark flex items-center justify-between border-l border-[#d1d7db]"> <div class="w-full flex flex-col">
<div class="flex items-center"> <div class="p-3 bg-whatsapp-dark flex items-center justify-between border-l border-[#d1d7db]">
{% if not no_avatar %} <div class="flex items-center">
<div class="w3-col m2 l2"> {% if not no_avatar %}
{% if their_avatar is not none %} <div class="w3-col m2 l2">
<a href="{{ their_avatar }}"><img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="w-10 h-10 rounded-full mr-3" loading="lazy"></a> {% if their_avatar is not none %}
{% else %} <a href="{{ their_avatar }}"><img src="{{ their_avatar_thumb or '' }}"
<img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="w-10 h-10 rounded-full mr-3" loading="lazy"> onerror="this.style.display='none'" class="w-10 h-10 rounded-full mr-3"
{% endif %} loading="lazy"></a>
</div> {% else %}
{% endif %} <img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'"
<div> class="w-10 h-10 rounded-full mr-3" loading="lazy">
<h2 class="text-white font-medium">{{ headline }}</h2>
{% if status is not none %}<p class="text-[#8696a0] text-xs">{{ status }}</p>{% endif %}
</div>
</div>
<div class="flex space-x-4">
<!-- <button id="searchButton">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button> -->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg> -->
{% if previous %}
<a href="./{{ previous }}" target="_self">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5l-7 7 7 7" />
</svg>
</a>
{% endif %}
{% if next %}
<a href="./{{ next }}" target="_self">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
{% endif %} {% endif %}
</div> </div>
<!-- Search Input Overlay --> {% endif %}
<div id="mainSearchInput" class="search-input absolute article top-0 bg-whatsapp-dark p-3 flex items-center space-x-3"> <div>
<button id="closeMainSearch" class="text-[#aebac1]"> <h2 class="text-white font-medium">{{ headline }}</h2>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {% if status is not none %}<p class="text-[#8696a0] text-xs">{{ status }}</p>{% endif %}
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<input type="text" placeholder="Search..." class="flex-1 bg-[#1f2c34] text-white rounded-lg px-3 py-1 focus:outline-none" id="mainHeaderSearchInput" onkeyup="search(event)">
</div> </div>
</div> </div>
<div class="flex space-x-4">
<!-- <button id="searchButton">
<span class="search-icon"></span>
</button> -->
<!-- <span class="arrow-left"></span> -->
{% if previous %}
<a href="./{{ previous }}" target="_self">
<span class="arrow-left"></span>
</a>
{% endif %}
{% if next %}
<a href="./{{ next }}" target="_self">
<span class="arrow-right"></span>
</a>
{% endif %}
</div>
<!-- Search Input Overlay -->
<div id="mainSearchInput"
class="search-input absolute article top-0 bg-whatsapp-dark p-3 flex items-center space-x-3">
<button id="closeMainSearch" class="text-[#aebac1]">
<span class="arrow-left"></span>
</button>
<input type="text" placeholder="Search..."
class="flex-1 bg-[#1f2c34] text-white rounded-lg px-3 py-1 focus:outline-none"
id="mainHeaderSearchInput" onkeyup="search(event)">
</div>
</div> </div>
<div class="flex-1 p-5 message-list"> </div>
<div class="flex flex-col space-y-2"> <div class="flex-1 p-5 message-list">
<!--Date--> <div class="flex flex-col space-y-2">
{% set last = {'last': 946688461.001} %} <!--Date-->
{% for msg in msgs -%} {% set last = {'last': 946688461.001} %}
{% if determine_day(last.last, msg.timestamp) is not none %} {% for msg in msgs -%}
<div class="flex justify-center"> {% if determine_day(last.last, msg.timestamp) is not none %}
<div class="bg-[#e1f2fb] rounded-lg px-2 py-1 text-xs text-[#54656f]"> <div class="flex justify-center">
{{ determine_day(last.last, msg.timestamp) }} <div class="bg-[#e1f2fb] rounded-lg px-2 py-1 text-xs text-[#54656f]">
</div> {{ determine_day(last.last, msg.timestamp) }}
</div> </div>
{% if last.update({'last': msg.timestamp}) %}{% endif %} </div>
{% endif %} {% if last.update({'last': msg.timestamp}) %}{% endif %}
<!--Actual messages--> {% endif %}
{% if msg.from_me == true %} <!--Actual messages-->
<div class="flex justify-end items-center group" id="{{ msg.key_id }}"> {% if msg.from_me == true %}
<div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative mr-2"> <div class="flex justify-end items-center group" id="{{ msg.key_id }}">
<div class="relative"> <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative mr-2">
<div class="relative group/tooltip"> <div class="relative">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#8696a0] hover:text-[#54656f] cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div class="relative group/tooltip">
<use href="#info-icon"></use> <span class="info-icon text-[#8696a0] hover:text-[#54656f] cursor-pointer"></span>
</svg> <div
<div class="absolute bottom-full info-box-tooltip mb-2 hidden group-hover/tooltip:block z-50"> class="absolute bottom-full info-box-tooltip mb-2 hidden group-hover/tooltip:block z-50">
<div class="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap"> <div class="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap">
Delivered at {{msg.received_timestamp or 'unknown'}} Delivered at {{msg.received_timestamp or 'unknown'}}
{% if msg.read_timestamp is not none %} {% if msg.read_timestamp is not none %}
<br>Read at {{ msg.read_timestamp }} <br>Read at {{ msg.read_timestamp }}
{% endif %} {% endif %}
</div> </div>
<div class="absolute top-full right-3 -mt-1 border-4 border-transparent border-t-black"></div> <div
</div> class="absolute top-full right-3 -mt-1 border-4 border-transparent border-t-black">
</div> </div>
</div> </div>
</div> </div>
<div class="bg-whatsapp-light rounded-lg p-2 max-w-[80%] shadow-sm">
{% if msg.reply is not none %}
<a href="#{{msg.reply}}" target="_self" class="no-base">
<div class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
<div class="flex items-center gap-2">
<div class="flex-1 overflow-hidden">
<p class="text-whatsapp font-medium text-xs">Replying to</p>
<p class="text-[#111b21] text-xs truncate">
{% if msg.quoted_data is not none %}
"{{msg.quoted_data}}"
{% else %}
this message
{% endif %}
</p>
</div>
{% set replied_msg = msgs | selectattr('key_id', 'equalto', msg.reply) | first %}
{% if replied_msg and replied_msg.media == true %}
<div class="flex-shrink-0">
{% if "image/" in replied_msg.mime %}
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}" class="w-8 h-8 rounded object-cover" loading="lazy"/>
{% elif "video/" in replied_msg.mime %}
<div class="relative w-8 h-8 rounded overflow-hidden bg-gray-200">
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}" class="w-full h-full object-cover" loading="lazy"/>
<div class="absolute inset-0 flex items-center justify-center">
<div class="play-icon"></div>
</div>
</div>
{% elif "audio/" in replied_msg.mime %}
<div class="w-8 h-8 rounded bg-gray-200 flex items-center justify-center">
<div class="speaker-icon"></div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</a>
{% endif %}
<p class="text-[#111b21] text-sm message-text">
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="flex justify-center mb-2">
<div class="bg-[#FFF3C5] rounded-lg px-3 py-2 text-sm text-[#856404] flex items-center">
{% if msg.safe %}
{{ msg.data | safe or 'Not supported WhatsApp internal message' }}
{% else %}
{{ msg.data or 'Not supported WhatsApp internal message' }}
{% endif %}
</div>
</div>
{% if msg.caption is not none %}
<p>{{ msg.caption | urlize(none, true, '_blank') }}</p>
{% endif %}
{% else %}
{% if msg.media == false %}
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }}
{% else %}
{% if "image/" in msg.mime %}
<a href="{{ msg.data }}">
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' | safe if msg.sticker }} loading="lazy"/>
</a>
{% elif "audio/" in msg.mime %}
<audio controls="controls" autobuffer="autobuffer">
<source src="{{ msg.data }}" />
</audio>
{% elif "video/" in msg.mime %}
<video class="lazy" autobuffer {% if msg.message_type|int == 13 or msg.message_type|int == 11 %}autoplay muted loop playsinline{%else%}controls{% endif %}>
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" />
</video>
{% elif "/" in msg.mime %}
The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a>
{% else %}
{% filter escape %}{{ msg.data }}{% endfilter %}
{% endif %}
{% if msg.caption is not none %}
{{ msg.caption | urlize(none, true, '_blank') }}
{% endif %}
{% endif %}
{% endif %}
</p>
<p class="text-[10px] text-[#667781] text-right mt-1">{{ msg.time }}
<span class="status-indicator{% if msg.read_timestamp %} read{% endif %}">
{% if msg.received_timestamp %}
✓✓
{% else %}
{% endif %}
</span>
</p>
</div>
</div> </div>
{% else %} </div>
<div class="flex justify-start items-center group" id="{{ msg.key_id }}"> <div class="bg-whatsapp-light rounded-lg p-2 max-w-[80%] shadow-sm">
<div class="bg-white rounded-lg p-2 max-w-[80%] shadow-sm"> {% if msg.reply is not none %}
{% if msg.reply is not none %} <a href="#{{msg.reply}}" target="_self" class="no-base">
<a href="#{{msg.reply}}" target="_self" class="no-base"> <div
<div class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box"> class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<p class="text-whatsapp font-medium text-xs">Replying to</p> <p class="text-whatsapp font-medium text-xs">Replying to</p>
<p class="text-[#808080] text-xs truncate"> <p class="text-[#111b21] text-xs truncate">
{% if msg.quoted_data is not none %} {% if msg.quoted_data is not none %}
{{msg.quoted_data}} "{{msg.quoted_data}}"
{% else %}
this message
{% endif %}
</p>
</div>
{% set replied_msg = msgs | selectattr('key_id', 'equalto', msg.reply) | first %}
{% if replied_msg and replied_msg.media == true %}
<div class="flex-shrink-0">
{% if "image/" in replied_msg.mime %}
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}" class="w-8 h-8 rounded object-cover" loading="lazy"/>
{% elif "video/" in replied_msg.mime %}
<div class="relative w-8 h-8 rounded overflow-hidden bg-gray-200">
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}" class="w-full h-full object-cover" loading="lazy"/>
<div class="absolute inset-0 flex items-center justify-center">
<div class="play-icon"></div>
</div>
</div>
{% elif "audio/" in replied_msg.mime %}
<div class="w-8 h-8 rounded bg-gray-200 flex items-center justify-center">
<div class="speaker-icon"></div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</a>
{% endif %}
<p class="text-[#111b21] text-sm">
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="flex justify-center mb-2">
<div class="bg-[#FFF3C5] rounded-lg px-3 py-2 text-sm text-[#856404] flex items-center">
{% if msg.safe %}
{{ msg.data | safe or 'Not supported WhatsApp internal message' }}
{% else %}
{{ msg.data or 'Not supported WhatsApp internal message' }}
{% endif %}
</div>
</div>
{% if msg.caption is not none %}
<p>{{ msg.caption | urlize(none, true, '_blank') }}</p>
{% endif %}
{% else %}
{% if msg.media == false %}
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }}
{% else %}
{% if "image/" in msg.mime %}
<a href="{{ msg.data }}">
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' | safe if msg.sticker }} loading="lazy"/>
</a>
{% elif "audio/" in msg.mime %}
<audio controls="controls" autobuffer="autobuffer">
<source src="{{ msg.data }}" />
</audio>
{% elif "video/" in msg.mime %}
<video class="lazy" autobuffer {% if msg.message_type|int == 13 or msg.message_type|int == 11 %}autoplay muted loop playsinline{%else%}controls{% endif %}>
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" />
</video>
{% elif "/" in msg.mime %}
The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a>
{% else %} {% else %}
{% filter escape %}{{ msg.data }}{% endfilter %} this message
{% endif %} {% endif %}
{% if msg.caption is not none %} </p>
{{ msg.caption | urlize(none, true, '_blank') }}
{% endif %}
{% endif %}
{% endif %}
</p>
<div class="flex items-baseline text-[10px] text-[#667781] mt-1 gap-2">
<span class="flex-shrink-0">
{% if msg.sender is not none %}
{{ msg.sender }}
{% endif %}
</span>
<span class="flex-grow min-w-[4px]"></span>
<span class="flex-shrink-0">{{ msg.time }}</span>
</div> </div>
{% set replied_msg = msgs | selectattr('key_id', 'equalto', msg.reply) | first %}
{% if replied_msg and replied_msg.media == true %}
<div class="flex-shrink-0">
{% if "image/" in replied_msg.mime %}
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}"
class="w-8 h-8 rounded object-cover" loading="lazy" />
{% elif "video/" in replied_msg.mime %}
<div class="relative w-8 h-8 rounded overflow-hidden bg-gray-200">
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}"
class="w-full h-full object-cover" loading="lazy" />
<div class="absolute inset-0 flex items-center justify-center">
<div class="play-icon"></div>
</div>
</div>
{% elif "audio/" in replied_msg.mime %}
<div class="w-8 h-8 rounded bg-gray-200 flex items-center justify-center">
<div class="speaker-icon"></div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div> </div>
<!-- <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative ml-2"> </a>
{% endif %}
<p class="text-[#111b21] text-sm message-text">
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="flex justify-center mb-2">
<div class="bg-[#FFF3C5] rounded-lg px-3 py-2 text-sm text-[#856404] flex items-center">
{% if msg.safe %}
{{ msg.data | safe or 'Not supported WhatsApp internal message' }}
{% else %}
{{ msg.data or 'Not supported WhatsApp internal message' }}
{% endif %}
</div>
</div>
{% if msg.caption is not none %}
<p>{{ msg.caption | urlize(none, true, '_blank') }}</p>
{% endif %}
{% else %}
{% if msg.media == false %}
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }}
{% else %}
{% if "image/" in msg.mime %}
<a href="{{ msg.data }}">
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' |
safe if msg.sticker }} loading="lazy" />
</a>
{% elif "audio/" in msg.mime %}
<audio controls="controls" autobuffer="autobuffer">
<source src="{{ msg.data }}" />
</audio>
{% elif "video/" in msg.mime %}
<video class="lazy" autobuffer {% if msg.message_type|int==13 or msg.message_type|int==11
%}autoplay muted loop playsinline{%else%}controls{% endif %}>
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" />
</video>
{% elif "/" in msg.mime %}
The file cannot be displayed here, however it should be located at <a
href="./{{ msg.data }}">here</a>
{% else %}
{% filter escape %}{{ msg.data }}{% endfilter %}
{% endif %}
{% if msg.caption is not none %}
{{ msg.caption | urlize(none, true, '_blank') }}
{% endif %}
{% endif %}
{% endif %}
</p>
<p class="text-[10px] text-[#667781] text-right mt-1">{{ msg.time }}
<span class="status-indicator{% if msg.read_timestamp %} read{% endif %}">
{% if msg.received_timestamp %}
✓✓
{% else %}
{% endif %}
</span>
</p>
</div>
</div>
{% else %}
<div class="flex justify-start items-center group" id="{{ msg.key_id }}">
<div class="bg-white rounded-lg p-2 max-w-[80%] shadow-sm">
{% if msg.reply is not none %}
<a href="#{{msg.reply}}" target="_self" class="no-base">
<div
class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
<div class="flex items-center gap-2">
<div class="flex-1 overflow-hidden">
<p class="text-whatsapp font-medium text-xs">Replying to</p>
<p class="text-[#808080] text-xs truncate">
{% if msg.quoted_data is not none %}
{{msg.quoted_data}}
{% else %}
this message
{% endif %}
</p>
</div>
{% set replied_msg = msgs | selectattr('key_id', 'equalto', msg.reply) | first %}
{% if replied_msg and replied_msg.media == true %}
<div class="flex-shrink-0">
{% if "image/" in replied_msg.mime %}
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}"
class="w-8 h-8 rounded object-cover" loading="lazy" />
{% elif "video/" in replied_msg.mime %}
<div class="relative w-8 h-8 rounded overflow-hidden bg-gray-200">
<img src="{{ replied_msg.thumb if replied_msg.thumb is not none else replied_msg.data }}"
class="w-full h-full object-cover" loading="lazy" />
<div class="absolute inset-0 flex items-center justify-center">
<div class="play-icon"></div>
</div>
</div>
{% elif "audio/" in replied_msg.mime %}
<div class="w-8 h-8 rounded bg-gray-200 flex items-center justify-center">
<div class="speaker-icon"></div>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</a>
{% endif %}
<p class="text-[#111b21] text-sm">
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="flex justify-center mb-2">
<div class="bg-[#FFF3C5] rounded-lg px-3 py-2 text-sm text-[#856404] flex items-center">
{% if msg.safe %}
{{ msg.data | safe or 'Not supported WhatsApp internal message' }}
{% else %}
{{ msg.data or 'Not supported WhatsApp internal message' }}
{% endif %}
</div>
</div>
{% if msg.caption is not none %}
<p>{{ msg.caption | urlize(none, true, '_blank') }}</p>
{% endif %}
{% else %}
{% if msg.media == false %}
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }}
{% else %}
{% if "image/" in msg.mime %}
<a href="{{ msg.data }}">
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' |
safe if msg.sticker }} loading="lazy" />
</a>
{% elif "audio/" in msg.mime %}
<audio controls="controls" autobuffer="autobuffer">
<source src="{{ msg.data }}" />
</audio>
{% elif "video/" in msg.mime %}
<video class="lazy" autobuffer {% if msg.message_type|int==13 or msg.message_type|int==11
%}autoplay muted loop playsinline{%else%}controls{% endif %}>
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" />
</video>
{% elif "/" in msg.mime %}
The file cannot be displayed here, however it should be located at <a
href="./{{ msg.data }}">here</a>
{% else %}
{% filter escape %}{{ msg.data }}{% endfilter %}
{% endif %}
{% if msg.caption is not none %}
{{ msg.caption | urlize(none, true, '_blank') }}
{% endif %}
{% endif %}
{% endif %}
</p>
<div class="flex items-baseline text-[10px] text-[#667781] mt-1 gap-2">
<span class="flex-shrink-0">
{% if msg.sender is not none %}
{{ msg.sender }}
{% endif %}
</span>
<span class="flex-grow min-w-[4px]"></span>
<span class="flex-shrink-0">{{ msg.time }}</span>
</div>
</div>
<!-- <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative ml-2">
<div class="relative"> <div class="relative">
<div class="relative group/tooltip"> <div class="relative group/tooltip">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#8696a0] hover:text-[#54656f] cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="info-icon text-[#8696a0] hover:text-[#54656f] cursor-pointer"></span>
<use href="#info-icon"></use>
</svg>
<div class="absolute bottom-full info-box-tooltip mb-2 hidden group-hover/tooltip:block z-50"> <div class="absolute bottom-full info-box-tooltip mb-2 hidden group-hover/tooltip:block z-50">
<div class="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap"> <div class="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap">
Received at {{msg.received_timestamp or 'unknown'}} Received at {{msg.received_timestamp or 'unknown'}}
@@ -472,29 +582,27 @@
</div> </div>
</div> </div>
</div> --> </div> -->
</div>
{% endif %}
{% endfor %}
</div> </div>
<footer> {% endif %}
<h2 class="text-center"> {% endfor %}
</div>
<footer>
<h2 class="text-center">
{% if not next %} {% if not next %}
End of History End of History
{% endif %} {% endif %}
</h2> </h2>
<br> <br>
Portions of this page are reproduced from <a href="https://web.dev/articles/lazy-loading-video">work</a> created and <a href="https://developers.google.com/readme/policies">shared by Google</a> and used according to terms described in the <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a>. Portions of this page are reproduced from <a href="https://web.dev/articles/lazy-loading-video">work</a>
</footer> created and <a href="https://developers.google.com/readme/policies">shared by Google</a> and used
<svg style="display: none;"> according to terms described in the <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0
<!-- Tooltip info icon --> License</a>.
<symbol id="info-icon" viewBox="0 0 24 24"> </footer>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</symbol> </div>
</svg> </article>
</div> </body>
</article> <script>
</body>
<script>
// Search functionality // Search functionality
const searchButton = document.getElementById('searchButton'); const searchButton = document.getElementById('searchButton');
const mainSearchInput = document.getElementById('mainSearchInput'); const mainSearchInput = document.getElementById('mainSearchInput');
@@ -523,45 +631,46 @@
hideSearch(); hideSearch();
} }
}); });
</script> </script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function () {
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
if ("IntersectionObserver" in window) { if ("IntersectionObserver" in window) {
var lazyVideoObserver = new IntersectionObserver(function(entries, observer) { var lazyVideoObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function(video) { entries.forEach(function (video) {
if (video.isIntersecting) { if (video.isIntersecting) {
for (var source in video.target.children) { for (var source in video.target.children) {
var videoSource = video.target.children[source]; var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") { if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src; videoSource.src = videoSource.dataset.src;
} }
} }
video.target.load(); video.target.load();
video.target.classList.remove("lazy"); video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target); lazyVideoObserver.unobserve(video.target);
} }
}); });
}); });
lazyVideos.forEach(function(lazyVideo) { lazyVideos.forEach(function (lazyVideo) {
lazyVideoObserver.observe(lazyVideo); lazyVideoObserver.observe(lazyVideo);
}); });
} }
}); });
</script> </script>
<script> <script>
// Prevent the <base> tag from affecting links with the class "no-base" // Prevent the <base> tag from affecting links with the class "no-base"
document.querySelectorAll('.no-base').forEach(link => { document.querySelectorAll('.no-base').forEach(link => {
link.addEventListener('click', function(event) { link.addEventListener('click', function (event) {
const href = this.getAttribute('href'); const href = this.getAttribute('href');
if (href.startsWith('#')) { if (href.startsWith('#')) {
window.location.hash = href; window.location.hash = href;
event.preventDefault(); event.preventDefault();
} }
}); });
}); });
</script> </script>
</html> </html>