Created
November 26, 2025 19:38
-
-
Save sunmeat/3bc67fbff8f07d405dd7268e6e37a2e1 to your computer and use it in GitHub Desktop.
telegram v.0.01 spring boot + firestore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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("<", "<")) | |
| .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