Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created November 26, 2025 19:38
Show Gist options
  • Select an option

  • Save sunmeat/3bc67fbff8f07d405dd7268e6e37a2e1 to your computer and use it in GitHub Desktop.

Select an option

Save sunmeat/3bc67fbff8f07d405dd7268e6e37a2e1 to your computer and use it in GitHub Desktop.
telegram v.0.01 spring boot + firestore
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.0-SNAPSHOT'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'site.sunmeat'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
maven { url = 'https://repo.spring.io/snapshot' }
google() // !!!
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'com.google.firebase:firebase-admin:+' // !!!
implementation 'org.webjars:htmx.org:2.0.0' // !!!
}
tasks.named('test') {
useJUnitPlatform()
}
===============================================================================================================
HibernateApplication.java:
package site.sunmeat.hibernate;
import java.awt.Desktop;
import java.io.*;
import java.net.URI;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.*;
@SpringBootApplication
public class HibernateApplication {
public static void main(String[] args) {
SpringApplication.run(HibernateApplication.class, args);
}
@Bean
public FirebaseApp initializeFirebase() throws IOException {
InputStream serviceAccount = getClass()
.getClassLoader()
.getResourceAsStream("firebase-service-account.json");
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(options);
}
return FirebaseApp.getInstance();
}
}
@Component
class BrowserLauncher {
@EventListener(ApplicationReadyEvent.class)
public void launchBrowser() {
System.setProperty("java.awt.headless", "false");
Desktop desktop = Desktop.getDesktop();
try {
desktop.browse(new URI("http://localhost:8080"));
} catch (Exception e) { }
}
}
===============================================================================================================
WebController.java:
package site.sunmeat.hibernate;
import com.google.cloud.firestore.*;
import com.google.firebase.cloud.FirestoreClient;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@DependsOn("initializeFirebase") // !!!
public class WebController {
private Firestore db;
private static final String COLLECTION = "chat_messages";
@PostConstruct
public void init() {
db = FirestoreClient.getFirestore();
}
@GetMapping("/messages")
@ResponseBody
public String getMessages() {
try {
var query = db.collection(COLLECTION)
.orderBy("timestamp", Query.Direction.ASCENDING)
.get()
.get();
var html = new StringBuilder();
for (DocumentSnapshot doc : query.getDocuments()) {
String login = doc.getString("login");
String text = doc.getString("message");
if (login == null || text == null) continue;
boolean isMe = "Sunmeat".equals(login.trim());
html.append("<div class='message ")
.append(isMe ? "me" : "other")
.append("'><div class='login'>")
.append(login.trim())
.append("</div><div>")
.append(text.trim().replace("<", "&lt;"))
.append("</div></div>");
}
return html.toString();
} catch (Exception e) {
e.printStackTrace();
return "<div style='color:#ff5555;text-align:center;padding:20px'>Помилка сервера</div>";
}
}
@PostMapping("/send")
@ResponseBody
public Map<String, String> send(
@RequestParam(name = "login") String login,
@RequestParam(name = "message") String message) {
if (message == null || message.trim().isEmpty()) {
return Map.of("status", "error");
}
Map<String, Object> data = new HashMap<>();
data.put("login", login.trim());
data.put("message", message.trim());
data.put("timestamp", FieldValue.serverTimestamp());
db.collection(COLLECTION).add(data);
return Map.of("status", "ok");
}
record Message(String login, String message) {}
}
===============================================================================================================
style.css:
:root {
--bg: #0e1621;
--header: #17212b;
--my-bubble: #005c4b;
--other-bubble: #182533;
--text: #ffffff;
--input-bg: #242f3d;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background: var(--header);
padding: 16px;
text-align: center;
font-size: 20px;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 72%;
padding: 10px 16px;
border-radius: 18px;
line-height: 1.4;
word-wrap: break-word;
align-self: flex-start;
background: var(--other-bubble);
}
.message.me {
align-self: flex-end;
background: var(--my-bubble);
color: white;
}
.login {
font-weight: 600;
font-size: 0.87em;
opacity: 0.9;
margin-bottom: 4px;
}
.input-area {
padding: 12px;
background: var(--header);
display: flex;
gap: 10px;
align-items: center;
}
#messageInput {
flex: 1;
padding: 14px 18px;
border: none;
border-radius: 30px;
background: var(--input-bg);
color: white;
font-size: 16px;
outline: none;
}
button {
padding: 0 24px;
height: 50px;
background: #0088cc;
color: white;
border: none;
border-radius: 30px;
cursor: pointer;
font-weight: 600;
min-width: 90px;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background: #444;
border-radius: 3px;
}
===============================================================================================================
index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Telegram Чат</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/[email protected]"></script>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="header">
<h1>Telegram Чат</h1>
</div>
<div id="messagesList"
class="messages"
hx-get="/messages"
hx-trigger="every 1s, sendMessage from:body"
hx-swap="innerHTML">
</div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="Напиши повідомлення..."
onkeypress="if(event.key==='Enter'){sendMessage(); this.value=''}">
<button onclick="sendMessage()">Надіслати</button>
</div>
<script>
let userScrolledUp = false;
let justSentMessage = false;
function sendMessage() {
const input = document.getElementById("messageInput");
const msg = input.value.trim();
if (!msg) return;
fetch("/send", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "login=Sunmeat&message=" + encodeURIComponent(msg)
}).then(() => {
input.value = "";
justSentMessage = true;
htmx.trigger("#messagesList", "sendMessage");
});
}
document.querySelector('.messages').addEventListener('scroll', function() {
const el = this;
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
userScrolledUp = !isAtBottom;
});
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target.id !== 'messagesList') return;
const chat = e.detail.target;
const shouldScroll = !userScrolledUp || justSentMessage;
if (shouldScroll) {
chat.scrollTop = chat.scrollHeight;
justSentMessage = false;
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment