Switch the default template to the WhatsApp-alike them

The old telegram theme can still be applied with the `--old-theme` option
This commit is contained in:
KnugiHK
2025-12-14 21:40:17 +08:00
parent beaf272a63
commit 194ed29a6e
4 changed files with 797 additions and 1008 deletions

View File

@@ -185,8 +185,8 @@ def setup_argument_parser() -> ArgumentParser:
help="Do not render avatar in HTML output" help="Do not render avatar in HTML output"
) )
html_group.add_argument( html_group.add_argument(
"--experimental-new-theme", dest="whatsapp_theme", default=False, action='store_true', "--old-theme", dest="telegram_theme", default=False, action='store_true',
help="Use the newly designed WhatsApp-alike theme" help="Use the old Telegram-alike theme"
) )
html_group.add_argument( html_group.add_argument(
"--headline", dest="headline", default="Chat history with ??", "--headline", dest="headline", default="Chat history with ??",
@@ -359,8 +359,8 @@ def validate_args(parser: ArgumentParser, args) -> None:
args.key = getpass("Enter your encryption key: ") args.key = getpass("Enter your encryption key: ")
# Theme validation # Theme validation
if args.whatsapp_theme: if args.telegram_theme:
args.template = "whatsapp_new.html" args.template = "whatsapp_old.html"
# Chat filter validation # Chat filter validation
if args.filter_chat_include is not None and args.filter_chat_exclude is not None: if args.filter_chat_include is not None and args.filter_chat_exclude is not None:
@@ -628,7 +628,7 @@ def create_output_files(args, data: ChatCollection) -> None:
args.offline, args.offline,
args.size, args.size,
args.no_avatar, args.no_avatar,
args.whatsapp_theme, args.telegram_theme,
args.headline args.headline
) )
@@ -713,7 +713,7 @@ def process_exported_chat(args, data: ChatCollection) -> None:
args.offline, args.offline,
args.size, args.size,
args.no_avatar, args.no_avatar,
args.whatsapp_theme, args.telegram_theme,
args.headline args.headline
) )
@@ -779,7 +779,7 @@ def main():
args.offline, args.offline,
args.size, args.size,
args.no_avatar, args.no_avatar,
args.whatsapp_theme, args.telegram_theme,
args.headline args.headline
) )
elif args.exported: elif args.exported:

View File

@@ -1,329 +1,467 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Whatsapp - {{ name }}</title> <title>Whatsapp - {{ name }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="{{w3css}}"> <script src="https://cdn.tailwindcss.com"></script>
<style> <script>
html, body { tailwind.config = {
font-size: 12px; theme: {
scroll-behavior: smooth; extend: {
} colors: {
header { whatsapp: {
position: fixed; light: '#e7ffdb',
z-index: 20; DEFAULT: '#25D366',
border-bottom: 2px solid #e3e6e7; dark: '#075E54',
font-size: 2em; chat: '#efeae2',
font-weight: bolder; 'chat-light': '#f0f2f5',
background-color: white; }
padding: 20px 0 20px 0; }
} }
footer { }
border-top: 2px solid #e3e6e7; }
padding: 20px 0 20px 0; </script>
} <style>
article { body, html {
width:500px; height: 100%;
margin:100px auto; margin: 0;
z-index:10; padding: 0;
font-size: 15px; scroll-behavior: smooth !important;
word-wrap: break-word; }
} .chat-list {
img, video { height: calc(100vh - 120px);
max-width:100%; overflow-y: auto;
} }
div.reply{ .message-list {
font-size: 13px; height: calc(100vh - 90px);
text-decoration: none; overflow-y: auto;
} }
div:target::before { @media (max-width: 640px) {
content: ''; .chat-list, .message-list {
display: block; height: calc(100vh - 108px);
height: 115px; }
margin-top: -115px; }
visibility: hidden; header {
} position: fixed;
div:target { z-index: 20;
border-style: solid; border-bottom: 2px solid #e3e6e7;
border-width: 2px; font-size: 2em;
animation: border-blink 0.5s steps(1) 5; font-weight: bolder;
border-color: rgba(0,0,0,0) background-color: white;
} padding: 20px 0 20px 0;
table { }
width: 100%; footer {
} margin-top: 10px;
@keyframes border-blink { border-top: 2px solid #e3e6e7;
0% { padding: 20px 0 20px 0;
border-color: #2196F3; }
} article {
50% { width:430px;
border-color: rgba(0,0,0,0); margin: auto;
} z-index:10;
} font-size: 15px;
.avatar { word-wrap: break-word;
border-radius:50%; }
overflow:hidden; img, video, audio{
max-width: 64px; max-width:100%;
max-height: 64px; box-sizing: border-box;
} }
.name { div.reply{
color: #3892da; font-size: 13px;
} text-decoration: none;
.pad-left-10 { }
padding-left: 10px; div:target::before {
} content: '';
.pad-right-10 { display: block;
padding-right: 10px; height: 115px;
} margin-top: -115px;
.reply_link { visibility: hidden;
color: #168acc; }
} div:target {
.blue { animation: 3s highlight;
color: #70777a; }
} .avatar {
.sticker { border-radius:50%;
max-width: 100px !important; overflow:hidden;
max-height: 100px !important; max-width: 64px;
} max-height: 64px;
</style> }
<base href="{{ media_base }}" target="_blank"> .name {
</head> color: #3892da;
<body> }
<header class="w3-center w3-top"> .pad-left-10 {
{{ headline }} padding-left: 10px;
{% if status is not none %} }
<br> .pad-right-10 {
<span class="w3-small">{{ status }}</span> padding-right: 10px;
{% endif %} }
</header> .reply_link {
<article class="w3-container"> color: #168acc;
<div class="table"> }
{% set last = {'last': 946688461.001} %} .blue {
{% for msg in msgs -%} color: #70777a;
<div class="w3-row w3-padding-small w3-margin-bottom" id="{{ msg.key_id }}"> }
{% if determine_day(last.last, msg.timestamp) is not none %} .sticker {
<div class="w3-center w3-padding-16 blue">{{ determine_day(last.last, msg.timestamp) }}</div> max-width: 100px !important;
{% if last.update({'last': msg.timestamp}) %}{% endif %} max-height: 100px !important;
{% endif %} }
{% if msg.from_me == true %} @keyframes highlight {
<div class="w3-row"> from {
<div class="w3-left blue">{{ msg.time }}</div> background-color: rgba(37, 211, 102, 0.1);
<div class="name w3-right-align pad-left-10">You</div> }
</div> to {
<div class="w3-row"> background-color: transparent;
{% if not no_avatar and my_avatar is not none %} }
<div class="w3-col m10 l10"> }
{% else %} .search-input {
<div class="w3-col m12 l12"> transform: translateY(-100%);
{% endif %} transition: transform 0.3s ease-in-out;
<div class="w3-right-align"> }
{% if msg.reply is not none %} .search-input.active {
<div class="reply"> transform: translateY(0);
<span class="blue">Replying to </span> }
<a href="#{{msg.reply}}" target="_self" class="reply_link no-base"> .reply-box:active {
{% if msg.quoted_data is not none %} background-color:rgb(200 202 205 / var(--tw-bg-opacity, 1));
"{{msg.quoted_data}}" }
{% else %} .info-box-tooltip {
this message --tw-translate-x: -50%;
{% endif %} 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));
</a> }
</div> </style>
{% endif %} <script>
{% if msg.meta == true or msg.media == false and msg.data is none %} function search(event) {
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center"> keywords = document.getElementById("mainHeaderSearchInput").value;
{% if msg.safe %} hits = [];
<p>{{ msg.data | safe or 'Not supported WhatsApp internal message' }}</p> document.querySelectorAll(".message-text").forEach(elem => {
{% else %} if (elem.innerText.trim().includes(keywords)){
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p> hits.push(elem.parentElement.parentElement.id);
{% endif %} }
</div> })
{% if msg.caption is not none %} console.log(hits);
<div class="w3-container"> }
{{ msg.caption | urlize(none, true, '_blank') }} </script>
</div> <base href="{{ media_base }}" target="_blank">
{% endif %} </head>
{% else %} <body>
{% if msg.media == false %} <article class="h-screen bg-whatsapp-chat-light">
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }} <div class="w-full flex flex-col">
{% else %} <div class="p-3 bg-whatsapp-dark flex items-center justify-between border-l border-[#d1d7db]">
{% if "image/" in msg.mime %} <div class="flex items-center">
<a href="{{ msg.data }}"> {% if not no_avatar %}
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' | safe if msg.sticker }} loading="lazy"/> <div class="w3-col m2 l2">
</a> {% if their_avatar is not none %}
{% elif "audio/" in msg.mime %} <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>
<audio controls="controls" autobuffer="autobuffer"> {% else %}
<source src="{{ msg.data }}" /> <img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="w-10 h-10 rounded-full mr-3" loading="lazy">
</audio> {% endif %}
{% elif "video/" in msg.mime %} </div>
<video class="lazy" autobuffer {% if msg.message_type|int == 13 or msg.message_type|int == 11 %}autoplay muted loop playsinline{%else%}controls{% endif %}> {% endif %}
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" /> <div>
</video> <h2 class="text-white font-medium">{{ headline }}</h2>
{% elif "/" in msg.mime %} {% if status is not none %}<p class="text-[#8696a0] text-xs">{{ status }}</p>{% endif %}
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center"> </div>
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p> </div>
</div> <div class="flex space-x-4">
{% else %} <!-- <button id="searchButton">
{% filter escape %}{{ msg.data }}{% endfilter %} <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{% endif %} <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" />
{% if msg.caption is not none %} </svg>
<div class="w3-container"> </button> -->
{{ msg.caption | urlize(none, true, '_blank') }} <!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
{% endif %} </svg> -->
{% endif %} {% if previous %}
{% endif %} <a href="./{{ previous }}" target="_self">
</div> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5l-7 7 7 7" />
{% if not no_avatar and my_avatar is not none %} </svg>
<div class="w3-col m2 l2 pad-left-10"> </a>
<a href="{{ my_avatar }}"> {% endif %}
<img src="{{ my_avatar }}" onerror="this.style.display='none'" class="avatar" loading="lazy"> {% if next %}
</a> <a href="./{{ next }}" target="_self">
</div> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-[#aebac1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{% endif %} <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</div> </svg>
{% else %} </a>
<div class="w3-row"> {% endif %}
<div class="w3-left pad-right-10 name"> </div>
{% if msg.sender is not none %} <!-- Search Input Overlay -->
{{ msg.sender }} <div id="mainSearchInput" class="search-input absolute article top-0 bg-whatsapp-dark p-3 flex items-center space-x-3">
{% else %} <button id="closeMainSearch" class="text-[#aebac1]">
{{ name }} <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
{% endif %} <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</div> </svg>
<div class="w3-right-align blue">{{ msg.time }}</div> </button>
</div> <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 class="w3-row"> </div>
{% if not no_avatar %} </div>
<div class="w3-col m2 l2"> </div>
{% if their_avatar is not none %} <div class="flex-1 p-5 message-list">
<a href="{{ their_avatar }}"><img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar" loading="lazy"></a> <div class="flex flex-col space-y-2">
{% else %} <!--Date-->
<img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar" loading="lazy"> {% set last = {'last': 946688461.001} %}
{% endif %} {% for msg in msgs -%}
</div> {% if determine_day(last.last, msg.timestamp) is not none %}
<div class="w3-col m10 l10"> <div class="flex justify-center">
{% else %} <div class="bg-[#e1f2fb] rounded-lg px-2 py-1 text-xs text-[#54656f]">
<div class="w3-col m12 l12"> {{ determine_day(last.last, msg.timestamp) }}
{% endif %} </div>
<div class="w3-left-align"> </div>
{% if msg.reply is not none %} {% if last.update({'last': msg.timestamp}) %}{% endif %}
<div class="reply"> {% endif %}
<span class="blue">Replying to </span> <!--Actual messages-->
<a href="#{{msg.reply}}" target="_self" class="reply_link no-base"> {% if msg.from_me == true %}
{% if msg.quoted_data is not none %} <div class="flex justify-end items-center group" id="{{ msg.key_id }}">
"{{msg.quoted_data}}" <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative mr-2">
{% else %} <div class="relative">
this message <div class="relative group/tooltip">
{% endif %} <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">
</a> <use href="#info-icon"></use>
</div> </svg>
{% endif %} <div class="absolute bottom-full info-box-tooltip mb-2 hidden group-hover/tooltip:block z-50">
{% if msg.meta == true or msg.media == false and msg.data is none %} <div class="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap">
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center"> Delivered at {{msg.received_timestamp or 'unknown'}}
{% if msg.safe %} {% if msg.read_timestamp is not none %}
<p>{{ msg.data | safe or 'Not supported WhatsApp internal message' }}</p> <br>Read at {{ msg.read_timestamp }}
{% else %} {% endif %}
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p> </div>
{% endif %} <div class="absolute top-full right-3 -mt-1 border-4 border-transparent border-t-black"></div>
</div> </div>
{% if msg.caption is not none %} </div>
<div class="w3-container"> </div>
{{ msg.caption | urlize(none, true, '_blank') }} </div>
</div> <div class="bg-whatsapp-light rounded-lg p-2 max-w-[80%] shadow-sm">
{% endif %} {% if msg.reply is not none %}
{% else %} <a href="#{{msg.reply}}" target="_self" class="no-base">
{% if msg.media == false %} <div class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
{{ msg.data | sanitize_except() | urlize(none, true, '_blank') }} <p class="text-whatsapp font-medium text-xs">Replying to</p>
{% else %} <p class="text-[#111b21] text-xs truncate">
{% if "image/" in msg.mime %} {% if msg.quoted_data is not none %}
<a href="{{ msg.data }}"> "{{msg.quoted_data}}"
<img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' | safe if msg.sticker }} loading="lazy"/> {% else %}
</a> this message
{% elif "audio/" in msg.mime %} {% endif %}
<audio controls="controls" autobuffer="autobuffer"> </p>
<source src="{{ msg.data }}" /> </div>
</audio> </a>
{% elif "video/" in msg.mime %} {% endif %}
<video class="lazy" autobuffer {% if msg.message_type|int == 13 or msg.message_type|int == 11 %}autoplay muted loop playsinline{%else%}controls{% endif %}> <p class="text-[#111b21] text-sm message-text">
<source type="{{ msg.mime }}" data-src="{{ msg.data }}" /> {% if msg.meta == true or msg.media == false and msg.data is none %}
</video> <div class="flex justify-center mb-2">
{% elif "/" in msg.mime %} <div class="bg-[#FFF3C5] rounded-lg px-3 py-2 text-sm text-[#856404] flex items-center">
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center"> {% if msg.safe %}
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p> {{ msg.data | safe or 'Not supported WhatsApp internal message' }}
</div> {% else %}
{% else %} {{ msg.data or 'Not supported WhatsApp internal message' }}
{% filter escape %}{{ msg.data }}{% endfilter %} {% endif %}
{% endif %} </div>
{% if msg.caption is not none %} </div>
<div class="w3-container"> {% if msg.caption is not none %}
{{ msg.caption | urlize(none, true, '_blank') }} <p>{{ msg.caption | urlize(none, true, '_blank') }}</p>
</div> {% endif %}
{% endif %} {% else %}
{% endif %} {% if msg.media == false %}
{% endif %} {{ msg.data | sanitize_except() | urlize(none, true, '_blank') }}
</div> {% else %}
</div> {% if "image/" in msg.mime %}
</div> <a href="{{ msg.data }}">
{% endif %} <img src="{{ msg.thumb if msg.thumb is not none else msg.data }}" {{ 'class="sticker"' | safe if msg.sticker }} loading="lazy"/>
</div> </a>
{% endfor %} {% elif "audio/" in msg.mime %}
</div> <audio controls="controls" autobuffer="autobuffer">
</article> <source src="{{ msg.data }}" />
<footer class="w3-center"> </audio>
<h2> {% elif "video/" in msg.mime %}
{% if previous %} <video class="lazy" autobuffer {% if msg.message_type|int == 13 or msg.message_type|int == 11 %}autoplay muted loop playsinline{%else%}controls{% endif %}>
<a href="./{{ previous }}" target="_self">Previous</a> <source type="{{ msg.mime }}" data-src="{{ msg.data }}" />
{% endif %} </video>
<h2> {% elif "/" in msg.mime %}
{% if next %} The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a>
<a href="./{{ next }}" target="_self">Next</a> {% else %}
{% else %} {% filter escape %}{{ msg.data }}{% endfilter %}
End of History {% endif %}
{% endif %} {% if msg.caption is not none %}
</h2> {{ msg.caption | urlize(none, true, '_blank') }}
<br> {% endif %}
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>. {% endif %}
</footer> {% endif %}
<script> </p>
document.addEventListener("DOMContentLoaded", function() { <p class="text-[10px] text-[#667781] text-right mt-1">{{ msg.time }}</p>
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy")); </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">
<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>
</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 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">
<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="bg-black text-white text-xs rounded py-1 px-2 whitespace-nowrap">
Received at {{msg.received_timestamp or 'unknown'}}
</div>
<div class="absolute top-full right-3 ml-1 border-4 border-transparent border-t-black"></div>
</div>
</div>
</div>
</div> -->
</div>
{% endif %}
{% endfor %}
</div>
<footer>
<h2 class="text-center">
{% if not next %}
End of History
{% endif %}
</h2>
<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>.
</footer>
<svg style="display: none;">
<!-- Tooltip info icon -->
<symbol id="info-icon" viewBox="0 0 24 24">
<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>
</svg>
</div>
</article>
</body>
<script>
// Search functionality
const searchButton = document.getElementById('searchButton');
const mainSearchInput = document.getElementById('mainSearchInput');
const closeMainSearch = document.getElementById('closeMainSearch');
const mainHeaderSearchInput = document.getElementById('mainHeaderSearchInput');
if ("IntersectionObserver" in window) { // Function to show search input
var lazyVideoObserver = new IntersectionObserver(function(entries, observer) { const showSearch = () => {
entries.forEach(function(video) { mainSearchInput.classList.add('active');
if (video.isIntersecting) { mainHeaderSearchInput.focus();
for (var source in video.target.children) { };
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}
video.target.load(); // Function to hide search input
video.target.classList.remove("lazy"); const hideSearch = () => {
lazyVideoObserver.unobserve(video.target); mainSearchInput.classList.remove('active');
} mainHeaderSearchInput.value = '';
}); };
});
lazyVideos.forEach(function(lazyVideo) { // Event listeners
lazyVideoObserver.observe(lazyVideo); searchButton.addEventListener('click', showSearch);
}); closeMainSearch.addEventListener('click', hideSearch);
}
}); // Handle ESC key
</script> document.addEventListener('keydown', (event) => {
<script> if (event.key === 'Escape' && mainSearchInput.classList.contains('active')) {
// Prevent the <base> tag from affecting links with the class "no-base" hideSearch();
document.querySelectorAll('.no-base').forEach(link => { }
link.addEventListener('click', function(event) { });
const href = this.getAttribute('href'); </script>
if (href.startsWith('#')) { <script>
window.location.hash = href; document.addEventListener("DOMContentLoaded", function() {
event.preventDefault(); var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
}
}); if ("IntersectionObserver" in window) {
}); var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
</script> entries.forEach(function(video) {
</body> if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}
video.target.load();
video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target);
}
});
});
lazyVideos.forEach(function(lazyVideo) {
lazyVideoObserver.observe(lazyVideo);
});
}
});
</script>
<script>
// Prevent the <base> tag from affecting links with the class "no-base"
document.querySelectorAll('.no-base').forEach(link => {
link.addEventListener('click', function(event) {
const href = this.getAttribute('href');
if (href.startsWith('#')) {
window.location.hash = href;
event.preventDefault();
}
});
});
</script>
</html> </html>

View File

@@ -1,678 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Whatsapp - {{ name }}</title>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
whatsapp: {
light: '#e7ffdb',
DEFAULT: '#25D366',
dark: '#075E54',
chat: '#efeae2',
'chat-light': '#f0f2f5',
}
}
}
}
}
</script>
<style>
body,
html {
height: 100%;
margin: 0;
padding: 0;
scroll-behavior: smooth !important;
}
.chat-list {
height: calc(100vh - 120px);
overflow-y: auto;
}
.message-list {
height: calc(100vh - 90px);
overflow-y: auto;
}
@media (max-width: 640px) {
.chat-list,
.message-list {
height: calc(100vh - 108px);
}
}
header {
position: fixed;
z-index: 20;
border-bottom: 2px solid #e3e6e7;
font-size: 2em;
font-weight: bolder;
background-color: white;
padding: 20px 0 20px 0;
}
footer {
margin-top: 10px;
border-top: 2px solid #e3e6e7;
padding: 20px 0 20px 0;
}
article {
width: 430px;
margin: auto;
z-index: 10;
font-size: 15px;
word-wrap: break-word;
}
img,
video,
audio {
max-width: 100%;
box-sizing: border-box;
}
div.reply {
font-size: 13px;
text-decoration: none;
}
div:target::before {
content: '';
display: block;
height: 115px;
margin-top: -115px;
visibility: hidden;
}
div:target {
animation: 3s highlight;
}
.avatar {
border-radius: 50%;
overflow: hidden;
max-width: 64px;
max-height: 64px;
}
.name {
color: #3892da;
}
.pad-left-10 {
padding-left: 10px;
}
.pad-right-10 {
padding-right: 10px;
}
.reply_link {
color: #168acc;
}
.blue {
color: #70777a;
}
.sticker {
max-width: 100px !important;
max-height: 100px !important;
}
@keyframes highlight {
from {
background-color: rgba(37, 211, 102, 0.1);
}
to {
background-color: transparent;
}
}
.search-input {
transform: translateY(-100%);
transition: transform 0.3s ease-in-out;
}
.search-input.active {
transform: translateY(0);
}
.reply-box:active {
background-color: rgb(200 202 205 / var(--tw-bg-opacity, 1));
}
.info-box-tooltip {
--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));
}
.status-indicator {
display: inline-block;
margin-left: 4px;
font-size: 0.8em;
color: #8c8c8c;
}
.status-indicator.read {
color: #34B7F1;
}
.play-icon {
width: 0;
height: 0;
border-left: 8px solid white;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
}
.speaker-icon {
position: relative;
width: 8px;
height: 6px;
background: #666;
border-radius: 1px 0 0 1px;
}
.speaker-icon::before {
content: '';
position: absolute;
right: -4px;
top: -1px;
width: 0;
height: 0;
border-left: 4px solid #666;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
}
.speaker-icon::after {
content: '';
position: absolute;
right: -8px;
top: -3px;
width: 8px;
height: 12px;
border: 2px solid #666;
border-left: none;
border-radius: 0 8px 8px 0;
}
.search-icon {
width: 20px;
height: 20px;
position: relative;
display: inline-block;
}
.search-icon::before {
content: '';
position: absolute;
width: 12px;
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">
</head>
<body>
<article class="h-screen bg-whatsapp-chat-light">
<div class="w-full flex flex-col">
<div class="p-3 bg-whatsapp-dark flex items-center justify-between border-l border-[#d1d7db]">
<div class="flex items-center">
{% if not no_avatar %}
<div class="w3-col m2 l2">
{% if their_avatar is not none %}
<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>
{% else %}
<img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'"
class="w-10 h-10 rounded-full mr-3" loading="lazy">
{% endif %}
</div>
{% endif %}
<div>
<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">
<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 class="flex flex-col space-y-2">
<!--Date-->
{% set last = {'last': 946688461.001} %}
{% for msg in msgs -%}
{% if determine_day(last.last, msg.timestamp) is not none %}
<div class="flex justify-center">
<div class="bg-[#e1f2fb] rounded-lg px-2 py-1 text-xs text-[#54656f]">
{{ determine_day(last.last, msg.timestamp) }}
</div>
</div>
{% if last.update({'last': msg.timestamp}) %}{% endif %}
{% endif %}
<!--Actual messages-->
{% if msg.from_me == true %}
<div class="flex justify-end items-center group" id="{{ msg.key_id }}">
<div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative mr-2">
<div class="relative">
<div class="relative group/tooltip">
<span class="info-icon text-[#8696a0] hover:text-[#54656f] cursor-pointer"></span>
<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">
Delivered at {{msg.received_timestamp or 'unknown'}}
{% if msg.read_timestamp is not none %}
<br>Read at {{ msg.read_timestamp }}
{% endif %}
</div>
<div
class="absolute top-full right-3 -mt-1 border-4 border-transparent border-t-black">
</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>
{% 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 group/tooltip">
<span class="info-icon text-[#8696a0] hover:text-[#54656f] cursor-pointer"></span>
<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">
Received at {{msg.received_timestamp or 'unknown'}}
</div>
<div class="absolute top-full right-3 ml-1 border-4 border-transparent border-t-black"></div>
</div>
</div>
</div>
</div> -->
</div>
{% endif %}
{% endfor %}
</div>
<footer>
{% if not next %}
<div class="flex justify-center mb-6">
<div class="bg-[#e1f2fb] rounded-lg px-3 py-2 text-sm text-[#54656f]">
End of History
</div>
</div>
{% endif %}
<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>.
</footer>
</div>
</article>
</body>
<script>
// Search functionality
const searchButton = document.getElementById('searchButton');
const mainSearchInput = document.getElementById('mainSearchInput');
const closeMainSearch = document.getElementById('closeMainSearch');
const mainHeaderSearchInput = document.getElementById('mainHeaderSearchInput');
// Function to show search input
const showSearch = () => {
mainSearchInput.classList.add('active');
mainHeaderSearchInput.focus();
};
// Function to hide search input
const hideSearch = () => {
mainSearchInput.classList.remove('active');
mainHeaderSearchInput.value = '';
};
// Event listeners
searchButton.addEventListener('click', showSearch);
closeMainSearch.addEventListener('click', hideSearch);
// Handle ESC key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && mainSearchInput.classList.contains('active')) {
hideSearch();
}
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function () {
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
if ("IntersectionObserver" in window) {
var lazyVideoObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (video) {
if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}
video.target.load();
video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target);
}
});
});
lazyVideos.forEach(function (lazyVideo) {
lazyVideoObserver.observe(lazyVideo);
});
}
});
</script>
<script>
// Prevent the <base> tag from affecting links with the class "no-base"
document.querySelectorAll('.no-base').forEach(link => {
link.addEventListener('click', function (event) {
const href = this.getAttribute('href');
if (href.startsWith('#')) {
window.location.hash = href;
event.preventDefault();
}
});
});
</script>
</html>

View File

@@ -0,0 +1,329 @@
<!DOCTYPE html>
<html>
<head>
<title>Whatsapp - {{ name }}</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="{{w3css}}">
<style>
html, body {
font-size: 12px;
scroll-behavior: smooth;
}
header {
position: fixed;
z-index: 20;
border-bottom: 2px solid #e3e6e7;
font-size: 2em;
font-weight: bolder;
background-color: white;
padding: 20px 0 20px 0;
}
footer {
border-top: 2px solid #e3e6e7;
padding: 20px 0 20px 0;
}
article {
width:500px;
margin:100px auto;
z-index:10;
font-size: 15px;
word-wrap: break-word;
}
img, video {
max-width:100%;
}
div.reply{
font-size: 13px;
text-decoration: none;
}
div:target::before {
content: '';
display: block;
height: 115px;
margin-top: -115px;
visibility: hidden;
}
div:target {
border-style: solid;
border-width: 2px;
animation: border-blink 0.5s steps(1) 5;
border-color: rgba(0,0,0,0)
}
table {
width: 100%;
}
@keyframes border-blink {
0% {
border-color: #2196F3;
}
50% {
border-color: rgba(0,0,0,0);
}
}
.avatar {
border-radius:50%;
overflow:hidden;
max-width: 64px;
max-height: 64px;
}
.name {
color: #3892da;
}
.pad-left-10 {
padding-left: 10px;
}
.pad-right-10 {
padding-right: 10px;
}
.reply_link {
color: #168acc;
}
.blue {
color: #70777a;
}
.sticker {
max-width: 100px !important;
max-height: 100px !important;
}
</style>
<base href="{{ media_base }}" target="_blank">
</head>
<body>
<header class="w3-center w3-top">
{{ headline }}
{% if status is not none %}
<br>
<span class="w3-small">{{ status }}</span>
{% endif %}
</header>
<article class="w3-container">
<div class="table">
{% set last = {'last': 946688461.001} %}
{% for msg in msgs -%}
<div class="w3-row w3-padding-small w3-margin-bottom" id="{{ msg.key_id }}">
{% if determine_day(last.last, msg.timestamp) is not none %}
<div class="w3-center w3-padding-16 blue">{{ determine_day(last.last, msg.timestamp) }}</div>
{% if last.update({'last': msg.timestamp}) %}{% endif %}
{% endif %}
{% if msg.from_me == true %}
<div class="w3-row">
<div class="w3-left blue">{{ msg.time }}</div>
<div class="name w3-right-align pad-left-10">You</div>
</div>
<div class="w3-row">
{% if not no_avatar and my_avatar is not none %}
<div class="w3-col m10 l10">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div class="w3-right-align">
{% if msg.reply is not none %}
<div class="reply">
<span class="blue">Replying to </span>
<a href="#{{msg.reply}}" target="_self" class="reply_link no-base">
{% if msg.quoted_data is not none %}
"{{msg.quoted_data}}"
{% else %}
this message
{% endif %}
</a>
</div>
{% endif %}
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
{% if msg.safe %}
<p>{{ msg.data | safe or 'Not supported WhatsApp internal message' }}</p>
{% else %}
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p>
{% endif %}
</div>
{% if msg.caption is not none %}
<div class="w3-container">
{{ msg.caption | urlize(none, true, '_blank') }}
</div>
{% 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 %}
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p>
</div>
{% else %}
{% filter escape %}{{ msg.data }}{% endfilter %}
{% endif %}
{% if msg.caption is not none %}
<div class="w3-container">
{{ msg.caption | urlize(none, true, '_blank') }}
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
{% if not no_avatar and my_avatar is not none %}
<div class="w3-col m2 l2 pad-left-10">
<a href="{{ my_avatar }}">
<img src="{{ my_avatar }}" onerror="this.style.display='none'" class="avatar" loading="lazy">
</a>
</div>
{% endif %}
</div>
{% else %}
<div class="w3-row">
<div class="w3-left pad-right-10 name">
{% if msg.sender is not none %}
{{ msg.sender }}
{% else %}
{{ name }}
{% endif %}
</div>
<div class="w3-right-align blue">{{ msg.time }}</div>
</div>
<div class="w3-row">
{% if not no_avatar %}
<div class="w3-col m2 l2">
{% if their_avatar is not none %}
<a href="{{ their_avatar }}"><img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar" loading="lazy"></a>
{% else %}
<img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar" loading="lazy">
{% endif %}
</div>
<div class="w3-col m10 l10">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div class="w3-left-align">
{% if msg.reply is not none %}
<div class="reply">
<span class="blue">Replying to </span>
<a href="#{{msg.reply}}" target="_self" class="reply_link no-base">
{% if msg.quoted_data is not none %}
"{{msg.quoted_data}}"
{% else %}
this message
{% endif %}
</a>
</div>
{% endif %}
{% if msg.meta == true or msg.media == false and msg.data is none %}
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
{% if msg.safe %}
<p>{{ msg.data | safe or 'Not supported WhatsApp internal message' }}</p>
{% else %}
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p>
{% endif %}
</div>
{% if msg.caption is not none %}
<div class="w3-container">
{{ msg.caption | urlize(none, true, '_blank') }}
</div>
{% 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 %}
<div class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p>
</div>
{% else %}
{% filter escape %}{{ msg.data }}{% endfilter %}
{% endif %}
{% if msg.caption is not none %}
<div class="w3-container">
{{ msg.caption | urlize(none, true, '_blank') }}
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</article>
<footer class="w3-center">
<h2>
{% if previous %}
<a href="./{{ previous }}" target="_self">Previous</a>
{% endif %}
<h2>
{% if next %}
<a href="./{{ next }}" target="_self">Next</a>
{% else %}
End of History
{% endif %}
</h2>
<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>.
</footer>
<script>
document.addEventListener("DOMContentLoaded", function() {
var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
if ("IntersectionObserver" in window) {
var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(video) {
if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source];
if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
videoSource.src = videoSource.dataset.src;
}
}
video.target.load();
video.target.classList.remove("lazy");
lazyVideoObserver.unobserve(video.target);
}
});
});
lazyVideos.forEach(function(lazyVideo) {
lazyVideoObserver.observe(lazyVideo);
});
}
});
</script>
<script>
// Prevent the <base> tag from affecting links with the class "no-base"
document.querySelectorAll('.no-base').forEach(link => {
link.addEventListener('click', function(event) {
const href = this.getAttribute('href');
if (href.startsWith('#')) {
window.location.hash = href;
event.preventDefault();
}
});
});
</script>
</body>
</html>