diff --git a/ChatExport_2026-04-06/css/style.css b/ChatExport_2026-04-06/css/style.css
new file mode 100644
index 0000000..3189390
--- /dev/null
+++ b/ChatExport_2026-04-06/css/style.css
@@ -0,0 +1,752 @@
+body {
+ margin: 0;
+ font: 12px/18px 'Open Sans',"Lucida Grande","Lucida Sans Unicode",Arial,Helvetica,Verdana,sans-serif;
+}
+strong {
+ font-weight: 700;
+}
+code, kbd, pre, samp {
+ font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
+}
+code {
+ padding: 2px 4px;
+ font-size: 90%;
+ color: #c7254e;
+ background-color: #f9f2f4;
+ border-radius: 4px;
+}
+pre {
+ display: block;
+ margin: 0;
+ line-height: 1.42857143;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #333;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+ overflow: auto;
+ padding: 3px;
+ border: 1px solid #eee;
+ max-height: none;
+ font-size: inherit;
+}
+.clearfix:after {
+ content: " ";
+ visibility: hidden;
+ display: block;
+ height: 0;
+ clear: both;
+}
+.pull_left {
+ float: left;
+}
+.pull_right {
+ float: right;
+}
+.page_wrap {
+ background-color: #ffffff;
+ color: #000000;
+}
+.page_wrap a {
+ color: #168acd;
+ text-decoration: none;
+}
+.page_wrap a:hover {
+ text-decoration: underline;
+}
+.page_header {
+ position: fixed;
+ z-index: 10;
+ background-color: #ffffff;
+ width: 100%;
+ border-bottom: 1px solid #e3e6e8;
+}
+.page_header .content {
+ width: 480px;
+ margin: 0 auto;
+ border-radius: 0 !important;
+}
+.page_header a.content {
+ background-repeat: no-repeat;
+ background-position: 24px 21px;
+ background-size: 24px 24px;
+}
+.bold {
+ color: #212121;
+ font-weight: 700;
+}
+.details {
+ color: #70777b;
+}
+.page_header .content .text {
+ padding: 24px 24px 22px 24px;
+ font-size: 22px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.page_header a.content .text {
+ padding: 24px 24px 22px 82px;
+}
+.page_body {
+ padding-top: 64px;
+ width: 480px;
+ margin: 0 auto;
+}
+.page_about {
+ padding: 24px 24px;
+}
+.with_divider {
+ border-top: 1px solid #e3e6e8;
+}
+.userpic_link {
+ display: block;
+ text-decoration: none;
+}
+.userpic_link:hover {
+ text-decoration: none;
+}
+.userpic {
+ display: block;
+ border-radius: 50%;
+ overflow: hidden;
+}
+.story {
+ display: block;
+ border-radius: 4px;
+ overflow: hidden;
+}
+.userpic .initials {
+ display: block;
+ color: #fff;
+ text-align: center;
+ text-transform: uppercase;
+ user-select: none;
+}
+.color_red,
+.userpic1,
+.media_call .fill,
+.media_file .fill,
+.media_live_location .fill {
+ background-color: #ff5555;
+}
+.color_green,
+.userpic2,
+.media_call.success .fill,
+.media_photo .fill {
+ background-color: #64bf47;
+}
+.color_yellow,
+.userpic3,
+.media_venue .fill {
+ background-color: #ffab00;
+}
+.color_blue,
+.userpic4,
+.media_audio_file .fill,
+.media_voice_message .fill {
+ background-color: #4f9cd9;
+}
+.color_purple,
+.userpic5,
+.media_game .fill {
+ background-color: #9884e8;
+}
+.color_pink,
+.userpic6,
+.media_invoice .fill {
+ background-color: #e671a5;
+}
+.color_sea,
+.userpic7,
+.media_location .fill,
+.media_video .fill {
+ background-color: #47bcd1;
+}
+.color_orange,
+.userpic8,
+.media_contact .fill {
+ background-color: #ff8c44;
+}
+.personal_info {
+ padding: 24px;
+}
+.personal_info .userpic .initials {
+ font-size: 30px;
+}
+.personal_info .rows {
+ float: left;
+ padding-right: 24px;
+}
+.personal_info .names {
+ width: 164px;
+}
+.personal_info .info {
+ width: 124px;
+}
+.personal_info .bio {
+ width: 400px;
+}
+.personal_info .row {
+ padding-bottom: 16px;
+}
+a.block_link {
+ display: block;
+ text-decoration: none !important;
+ border-radius: 4px;
+}
+a.block_link:hover {
+ text-decoration: none !important;
+ background-color: #f5f7f8;
+}
+a.expanded {
+ padding: 2px 8px;
+ margin: -2px -8px;
+}
+.sections {
+ padding: 11px 0;
+}
+.section {
+ height: 48px;
+ background-position: 24px 12px;
+ background-repeat: no-repeat;
+ background-size: 24px 24px;
+}
+.section .counter {
+ float: right;
+ padding: 14px 24px 0;
+ font-size: 15px;
+}
+.section .label {
+ padding: 15px 0 0 82px;
+ font-size: 15px;
+}
+.list_page .page_about {
+ padding: 16px 24px 0;
+ font-size: 11px;
+}
+.list_page .entry_list {
+ padding: 16px 0;
+}
+.list_page .entry {
+ padding: 10px 16px;
+}
+.list_page .entry .userpic .initials {
+ font-size: 18px;
+}
+.list_page .entry .body {
+ margin-left: 66px;
+}
+.list_page .entry .name {
+ padding: 4px 0 2px;
+ font-size: 14px;
+}
+.list_page .entry .subname {
+ padding-top: 4px;
+}
+.list_page .entry .details_entry {
+ padding-top: 4px;
+}
+.list_page .entry .info {
+ font-size: 11px;
+ padding-top: 5px;
+}
+.history {
+ padding: 16px 0;
+}
+.message {
+ margin: 0 -10px;
+ transition: background-color 2.0s ease;
+}
+div.selected {
+ background-color: rgba(242,246,250,255);
+ transition: background-color 0.5s ease;
+}
+.service {
+ padding: 10px 24px;
+}
+.service .body {
+ text-align: center;
+}
+.service .userpic_wrap {
+ padding-top: 10px;
+}
+.service .userpic {
+ margin: 0 auto;
+}
+.service .userpic .initials {
+ font-size: 24px;
+}
+.message .userpic .initials {
+ font-size: 16px;
+}
+.default {
+ padding: 10px;
+}
+.default.joined {
+ margin-top: -10px;
+}
+.default .from_name {
+ color: #3892db;
+ font-weight: 700;
+ padding-bottom: 5px;
+}
+.default .from_name .details {
+ font-weight: normal;
+}
+.default .body {
+ margin-left: 60px;
+}
+.default .text {
+ word-wrap: break-word;
+ line-height: 150%;
+ unicode-bidi: plaintext;
+ text-align: start;
+}
+.default .reply_to,
+.default .media_wrap {
+ padding-bottom: 5px;
+}
+.default .media {
+ margin: 0 -10px;
+ padding: 5px 10px;
+}
+.default .media .fill,
+.default .media .thumb {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+}
+.default .media .fill {
+ background-repeat: no-repeat;
+ background-position: 12px 12px;
+ background-size: 24px 24px;
+}
+.default .media .title,
+.default .media_poll .question {
+ padding-top: 4px;
+ font-size: 14px;
+}
+.default .media .description {
+ color: #000000;
+ padding-top: 4px;
+ font-size: 13px;
+}
+.default .media .status {
+ padding-top: 4px;
+ font-size: 13px;
+}
+.default .video_file_wrap,
+.default .animated_wrap {
+ position: relative;
+}
+.default .video_file,
+.default .animated,
+.default .photo,
+.default .sticker {
+ display: block;
+}
+.video_duration {
+ background: rgba(0, 0, 0, .4);
+ padding: 0px 5px;
+ position: absolute;
+ z-index: 2;
+ border-radius: 2px;
+ right: 3px;
+ bottom: 3px;
+ color: #ffffff;
+ font-size: 11px;
+}
+.video_play_bg {
+ background: rgba(0, 0, 0, .4);
+ width: 40px;
+ height: 40px;
+ line-height: 0;
+ position: absolute;
+ z-index: 2;
+ border-radius: 50%;
+ overflow: hidden;
+ margin: -20px auto 0 -20px;
+ top: 50%;
+ left: 50%;
+ pointer-events: none;
+}
+.video_play {
+ position: absolute;
+ display: inline-block;
+ top: 50%;
+ left: 50%;
+ margin-left: -5px;
+ margin-top: -9px;
+ z-index: 1;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 9px 0 9px 14px;
+ border-color: transparent transparent transparent #fff;
+}
+.gif_play {
+ font-weight: 700;
+ color: #FFF;
+ display: block;
+ line-height: 40px;
+ font-size: 13px;
+ text-align: center;
+}
+.pagination {
+ text-align: center;
+ padding: 20px;
+ font-size: 16px;
+}
+
+.toast_container {
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ opacity: 0;
+ transition: opacity 3.0s ease;
+}
+.toast_body {
+ margin: 0 -50%;
+ float: left;
+ border-radius: 15px;
+ padding: 10px 20px;
+ background: rgba(0, 0, 0, 0.7);
+ color: #ffffff;
+}
+div.toast_shown {
+ opacity: 1;
+ transition: opacity 0.4s ease;
+}
+
+.section.calls {
+ background-image: url(../images/section_calls.png);
+}
+.section.chats {
+ background-image: url(../images/section_chats.png);
+}
+.section.contacts {
+ background-image: url(../images/section_contacts.png);
+}
+.section.frequent {
+ background-image: url(../images/section_frequent.png);
+}
+.section.photos {
+ background-image: url(../images/section_photos.png);
+}
+.section.sessions {
+ background-image: url(../images/section_sessions.png);
+}
+.section.stories {
+ background-image: url(../images/section_stories.png);
+}
+.section.music {
+ background-image: url(../images/section_music.png);
+}
+.section.web {
+ background-image: url(../images/section_web.png);
+}
+.section.other {
+ background-image: url(../images/section_other.png)
+}
+.page_header a.content {
+ background-image: url(../images/back.png);
+}
+.media_call .fill {
+ background-image: url(../images/media_call.png)
+}
+.media_contact .fill {
+ background-image: url(../images/media_contact.png)
+}
+.media_file .fill {
+ background-image: url(../images/media_file.png)
+}
+.media_game .fill {
+ background-image: url(../images/media_game.png)
+}
+.media_live_location .fill,
+.media_location .fill,
+.media_venue .fill {
+ background-image: url(../images/media_location.png)
+}
+.media_audio_file .fill {
+ background-image: url(../images/media_music.png)
+}
+.media_invoice .fill {
+ background-image: url(../images/media_shop.png)
+}
+.media_voice_message .fill {
+ background-image: url(../images/media_voice.png)
+}
+.media_photo .fill {
+ background-image: url(../images/media_photo.png)
+}
+.media_video .fill {
+ background-image: url(../images/media_video.png)
+}
+.audio_icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background-color: #4f9cd9;
+ background-image: url(../images/media_music.png);
+ background-repeat: no-repeat;
+ background-position: 12px 12px;
+ background-size: 24px 24px;
+}
+
+@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
+.section.calls {
+ background-image: url(../images/section_calls@2x.png);
+}
+.section.chats {
+ background-image: url(../images/section_chats@2x.png);
+}
+.section.contacts {
+ background-image: url(../images/section_contacts@2x.png);
+}
+.section.frequent {
+ background-image: url(../images/section_frequent@2x.png);
+}
+.section.photos {
+ background-image: url(../images/section_photos@2x.png);
+}
+.section.sessions {
+ background-image: url(../images/section_sessions@2x.png);
+}
+.section.stories {
+ background-image: url(../images/section_stories@2x.png);
+}
+.section.music {
+ background-image: url(../images/section_music@2x.png);
+}
+.section.web {
+ background-image: url(../images/section_web@2x.png);
+}
+.section.other {
+ background-image: url(../images/section_other@2x.png);
+}
+.page_header a.content {
+ background-image: url(../images/back@2x.png);
+}
+.media_call .fill {
+ background-image: url(../images/media_call@2x.png)
+}
+.media_contact .fill {
+ background-image: url(../images/media_contact@2x.png)
+}
+.media_file .fill {
+ background-image: url(../images/media_file@2x.png)
+}
+.media_game .fill {
+ background-image: url(../images/media_game@2x.png)
+}
+.media_live_location .fill,
+.media_location .fill,
+.media_venue .fill {
+ background-image: url(../images/media_location@2x.png)
+}
+.media_audio_file .fill {
+ background-image: url(../images/media_music@2x.png)
+}
+.media_invoice .fill {
+ background-image: url(../images/media_shop@2x.png)
+}
+.media_voice_message .fill {
+ background-image: url(../images/media_voice@2x.png)
+}
+.media_photo .fill {
+ background-image: url(../images/media_photo@2x.png)
+}
+.media_video .fill {
+ background-image: url(../images/media_video@2x.png)
+}
+.audio_icon {
+ background-image: url(../images/media_music@2x.png);
+}
+}
+
+.spoiler {
+ background: #e8e8e8;
+}
+.spoiler.hidden {
+ background: #a9a9a9;
+ cursor: pointer;
+ border-radius: 3px;
+}
+.spoiler.hidden span {
+ opacity: 0;
+ user-select: none;
+}
+
+.bot_buttons_table {
+ border-spacing: 0px 2px;
+ width: 100%;
+}
+.bot_button {
+ border-radius: 8px;
+ text-align: center;
+ vertical-align: middle;
+ background-color: #168acd40;
+}
+.bot_button_row {
+ display: table;
+ table-layout: fixed;
+ padding: 0px;
+ width:100%;
+}
+.bot_button_row div {
+ display: table-cell;
+}
+.bot_button_column_separator {
+ width: 2px
+}
+
+.reactions {
+ margin: 5px 0;
+}
+
+.reactions .reaction {
+ display: inline-flex;
+ height: 20px;
+ border-radius: 15px;
+ background-color: #e8f5fc;
+ color: #168acd;
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.reactions .reaction.active {
+ background-color: #40a6e2;
+ color: #fff;
+}
+
+.reactions .reaction.paid {
+ background-color: #fdf6e1;
+ color: #c58523;
+}
+
+.reactions .reaction.active.paid {
+ background-color: #ecae0a;
+ color: #fdf6e1;
+}
+
+.reactions .reaction .emoji {
+ line-height: 20px;
+ margin: 0 5px;
+ font-size: 15px;
+}
+
+.reactions .reaction .userpic:not(:first-child) {
+ margin-left: -8px;
+}
+
+.reactions .reaction .userpic {
+ display: inline-block;
+}
+
+.reactions .reaction .userpic .initials {
+ font-size: 8px;
+}
+
+.reactions .reaction .count {
+ margin-right: 8px;
+ line-height: 20px;
+}
+
+@media (prefers-color-scheme: dark) {
+html, body {
+ background-color: #1a2026; /* groupCallBg */
+ margin: 0;
+ padding: 0;
+}
+.page_wrap {
+ background-color: #1a2026; /* groupCallBg */
+ color: #ffffff; /* groupCallMembersFg */
+ min-height: 100vh;
+}
+.page_wrap a {
+ color: #4db8ff; /* groupCallActiveFg */
+}
+.page_header {
+ background-color: #1a2026; /* groupCallBg */
+ border-bottom: 1px solid #2c333d; /* groupCallMembersBg */
+}
+.bold {
+ color: #ffffff; /* groupCallMembersFg */
+}
+.details {
+ color: #91979e; /* groupCallMemberNotJoinedStatus */
+}
+.page_body {
+ background-color: #1a2026; /* groupCallBg */
+}
+code {
+ color: #ff8aac; /* historyPeer6UserpicBg */
+ background-color: #2c333d; /* groupCallMembersBg */
+}
+pre {
+ color: #ffffff; /* groupCallMembersFg */
+ background-color: #2c333d; /* groupCallMembersBg */
+ border: 1px solid #323a45; /* groupCallMembersBgOver */
+}
+.with_divider {
+ border-top: 1px solid #2c333d; /* groupCallMembersBg */
+}
+a.block_link:hover {
+ background-color: #323a45; /* groupCallMembersBgOver */
+}
+.list_page .entry {
+ color: #ffffff; /* groupCallMembersFg */
+}
+.message {
+ color: #ffffff; /* groupCallMembersFg */
+}
+div.selected {
+ background-color: #323a45; /* groupCallMembersBgOver */
+}
+.default .from_name {
+ color: #4db8ff; /* groupCallActiveFg */
+}
+.default .media .description {
+ color: #ffffff; /* groupCallMembersFg */
+}
+msgInBg,
+.historyComposeAreaBg {
+ background-color: #2c333d; /* groupCallMembersBg */
+}
+msgOutBg {
+ background-color: #323a45; /* groupCallMembersBgOver */
+}
+msgInBgSelected {
+ background-color: #39424f; /* groupCallMembersBgRipple */
+}
+msgOutBgSelected {
+ background-color: #39424f; /* groupCallMembersBgRipple */
+}
+.spoiler {
+ background: #323a45; /* groupCallMembersBgOver */
+}
+.spoiler.hidden {
+ background: #61c0ff; /* groupCallMemberInactiveStatus */
+}
+.bot_button {
+ background-color: #4db8ff40; /* groupCallActiveFg with opacity */
+}
+.reactions .reaction {
+ background-color: #2c333d; /* groupCallMembersBg */
+ color: #4db8ff; /* groupCallActiveFg */
+}
+.reactions .reaction.active {
+ background-color: #4db8ff; /* groupCallActiveFg */
+ color: #1a2026; /* groupCallBg */
+}
+.reactions .reaction.paid {
+ background-color: #323a45; /* groupCallMembersBgOver */
+ color: #febb5b; /* historyPeer8UserpicBg */
+}
+.reactions .reaction.active.paid {
+ background-color: #febb5b; /* historyPeer8UserpicBg */
+ color: #1a2026; /* groupCallBg */
+}
+}
\ No newline at end of file
diff --git a/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3 b/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3
new file mode 100644
index 0000000..0020bae
Binary files /dev/null and b/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3 differ
diff --git a/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3 b/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3
new file mode 100644
index 0000000..7ced967
Binary files /dev/null and b/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3 differ
diff --git a/ChatExport_2026-04-06/images/back.png b/ChatExport_2026-04-06/images/back.png
new file mode 100644
index 0000000..846479f
Binary files /dev/null and b/ChatExport_2026-04-06/images/back.png differ
diff --git a/ChatExport_2026-04-06/images/back@2x.png b/ChatExport_2026-04-06/images/back@2x.png
new file mode 100644
index 0000000..03e3e58
Binary files /dev/null and b/ChatExport_2026-04-06/images/back@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_call.png b/ChatExport_2026-04-06/images/media_call.png
new file mode 100644
index 0000000..9614b95
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_call.png differ
diff --git a/ChatExport_2026-04-06/images/media_call@2x.png b/ChatExport_2026-04-06/images/media_call@2x.png
new file mode 100644
index 0000000..92cdbe6
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_call@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_contact.png b/ChatExport_2026-04-06/images/media_contact.png
new file mode 100644
index 0000000..be79172
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_contact.png differ
diff --git a/ChatExport_2026-04-06/images/media_contact@2x.png b/ChatExport_2026-04-06/images/media_contact@2x.png
new file mode 100644
index 0000000..e4be8b0
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_contact@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_file.png b/ChatExport_2026-04-06/images/media_file.png
new file mode 100644
index 0000000..3a5bde6
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_file.png differ
diff --git a/ChatExport_2026-04-06/images/media_file@2x.png b/ChatExport_2026-04-06/images/media_file@2x.png
new file mode 100644
index 0000000..ad2c6cc
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_file@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_game.png b/ChatExport_2026-04-06/images/media_game.png
new file mode 100644
index 0000000..cad8ab8
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_game.png differ
diff --git a/ChatExport_2026-04-06/images/media_game@2x.png b/ChatExport_2026-04-06/images/media_game@2x.png
new file mode 100644
index 0000000..09c2eb3
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_game@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_location.png b/ChatExport_2026-04-06/images/media_location.png
new file mode 100644
index 0000000..ab8080b
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_location.png differ
diff --git a/ChatExport_2026-04-06/images/media_location@2x.png b/ChatExport_2026-04-06/images/media_location@2x.png
new file mode 100644
index 0000000..11d88d5
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_location@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_music.png b/ChatExport_2026-04-06/images/media_music.png
new file mode 100644
index 0000000..057a694
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_music.png differ
diff --git a/ChatExport_2026-04-06/images/media_music@2x.png b/ChatExport_2026-04-06/images/media_music@2x.png
new file mode 100644
index 0000000..20c8056
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_music@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_photo.png b/ChatExport_2026-04-06/images/media_photo.png
new file mode 100644
index 0000000..d229387
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_photo.png differ
diff --git a/ChatExport_2026-04-06/images/media_photo@2x.png b/ChatExport_2026-04-06/images/media_photo@2x.png
new file mode 100644
index 0000000..e46794f
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_photo@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_shop.png b/ChatExport_2026-04-06/images/media_shop.png
new file mode 100644
index 0000000..4a92ce8
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_shop.png differ
diff --git a/ChatExport_2026-04-06/images/media_shop@2x.png b/ChatExport_2026-04-06/images/media_shop@2x.png
new file mode 100644
index 0000000..9cfe551
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_shop@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_video.png b/ChatExport_2026-04-06/images/media_video.png
new file mode 100644
index 0000000..b0a132a
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_video.png differ
diff --git a/ChatExport_2026-04-06/images/media_video@2x.png b/ChatExport_2026-04-06/images/media_video@2x.png
new file mode 100644
index 0000000..2b554b0
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_video@2x.png differ
diff --git a/ChatExport_2026-04-06/images/media_voice.png b/ChatExport_2026-04-06/images/media_voice.png
new file mode 100644
index 0000000..af33c2e
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_voice.png differ
diff --git a/ChatExport_2026-04-06/images/media_voice@2x.png b/ChatExport_2026-04-06/images/media_voice@2x.png
new file mode 100644
index 0000000..5356a37
Binary files /dev/null and b/ChatExport_2026-04-06/images/media_voice@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_calls.png b/ChatExport_2026-04-06/images/section_calls.png
new file mode 100644
index 0000000..adb42e2
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_calls.png differ
diff --git a/ChatExport_2026-04-06/images/section_calls@2x.png b/ChatExport_2026-04-06/images/section_calls@2x.png
new file mode 100644
index 0000000..eb2de58
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_calls@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_chats.png b/ChatExport_2026-04-06/images/section_chats.png
new file mode 100644
index 0000000..354ea9e
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_chats.png differ
diff --git a/ChatExport_2026-04-06/images/section_chats@2x.png b/ChatExport_2026-04-06/images/section_chats@2x.png
new file mode 100644
index 0000000..b7252a8
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_chats@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_contacts.png b/ChatExport_2026-04-06/images/section_contacts.png
new file mode 100644
index 0000000..1b94b8e
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_contacts.png differ
diff --git a/ChatExport_2026-04-06/images/section_contacts@2x.png b/ChatExport_2026-04-06/images/section_contacts@2x.png
new file mode 100644
index 0000000..64a213d
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_contacts@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_frequent.png b/ChatExport_2026-04-06/images/section_frequent.png
new file mode 100644
index 0000000..1656019
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_frequent.png differ
diff --git a/ChatExport_2026-04-06/images/section_frequent@2x.png b/ChatExport_2026-04-06/images/section_frequent@2x.png
new file mode 100644
index 0000000..ca9c3c8
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_frequent@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_music.png b/ChatExport_2026-04-06/images/section_music.png
new file mode 100644
index 0000000..a76a244
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_music.png differ
diff --git a/ChatExport_2026-04-06/images/section_music@2x.png b/ChatExport_2026-04-06/images/section_music@2x.png
new file mode 100644
index 0000000..53dc702
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_music@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_other.png b/ChatExport_2026-04-06/images/section_other.png
new file mode 100644
index 0000000..a60ed7a
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_other.png differ
diff --git a/ChatExport_2026-04-06/images/section_other@2x.png b/ChatExport_2026-04-06/images/section_other@2x.png
new file mode 100644
index 0000000..497fb33
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_other@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_photos.png b/ChatExport_2026-04-06/images/section_photos.png
new file mode 100644
index 0000000..3b511ae
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_photos.png differ
diff --git a/ChatExport_2026-04-06/images/section_photos@2x.png b/ChatExport_2026-04-06/images/section_photos@2x.png
new file mode 100644
index 0000000..587b4ff
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_photos@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_sessions.png b/ChatExport_2026-04-06/images/section_sessions.png
new file mode 100644
index 0000000..0354af4
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_sessions.png differ
diff --git a/ChatExport_2026-04-06/images/section_sessions@2x.png b/ChatExport_2026-04-06/images/section_sessions@2x.png
new file mode 100644
index 0000000..58b213e
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_sessions@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_stories.png b/ChatExport_2026-04-06/images/section_stories.png
new file mode 100644
index 0000000..650c69c
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_stories.png differ
diff --git a/ChatExport_2026-04-06/images/section_stories@2x.png b/ChatExport_2026-04-06/images/section_stories@2x.png
new file mode 100644
index 0000000..4292451
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_stories@2x.png differ
diff --git a/ChatExport_2026-04-06/images/section_web.png b/ChatExport_2026-04-06/images/section_web.png
new file mode 100644
index 0000000..d82e7fb
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_web.png differ
diff --git a/ChatExport_2026-04-06/images/section_web@2x.png b/ChatExport_2026-04-06/images/section_web@2x.png
new file mode 100644
index 0000000..b436884
Binary files /dev/null and b/ChatExport_2026-04-06/images/section_web@2x.png differ
diff --git a/ChatExport_2026-04-06/js/script.js b/ChatExport_2026-04-06/js/script.js
new file mode 100644
index 0000000..2842322
--- /dev/null
+++ b/ChatExport_2026-04-06/js/script.js
@@ -0,0 +1,211 @@
+"use strict";
+
+window.AllowBackFromHistory = false;
+function CheckLocation() {
+ var start = "#go_to_message";
+ var hash = location.hash;
+ if (hash.substr(0, start.length) == start) {
+ var messageId = parseInt(hash.substr(start.length));
+ if (messageId) {
+ GoToMessage(messageId);
+ }
+ } else if (hash == "#allow_back") {
+ window.AllowBackFromHistory = true;
+ }
+}
+
+function ShowToast(text) {
+ var container = document.createElement("div");
+ container.className = "toast_container";
+ var inner = container.appendChild(document.createElement("div"));
+ inner.className = "toast_body";
+ inner.appendChild(document.createTextNode(text));
+ var appended = document.body.appendChild(container);
+ setTimeout(function () {
+ AddClass(appended, "toast_shown");
+ setTimeout(function () {
+ RemoveClass(appended, "toast_shown");
+ setTimeout(function () {
+ document.body.removeChild(appended);
+ }, 3000);
+ }, 3000);
+ }, 0);
+}
+
+function ShowHashtag(tag) {
+ ShowToast("This is a hashtag '#" + tag + "' link.");
+ return false;
+}
+
+function ShowCashtag(tag) {
+ ShowToast("This is a cashtag '$" + tag + "' link.");
+ return false;
+}
+
+function ShowBotCommand(command) {
+ ShowToast("This is a bot command '/" + command + "' link.");
+ return false;
+}
+
+function ShowMentionName() {
+ ShowToast("This is a link to a user mentioned by name.");
+ return false;
+}
+
+function ShowNotLoadedEmoji() {
+ ShowToast("This custom emoji is not included, change data exporting settings to download.");
+ return false;
+}
+
+function ShowNotAvailableEmoji() {
+ ShowToast("This custom emoji is not available.");
+ return false;
+}
+
+function ShowTextCopied(content) {
+ navigator.clipboard.writeText(content);
+ ShowToast("Text copied to clipboard.");
+ return false;
+}
+
+function ShowSpoiler(target) {
+ if (target.classList.contains("hidden")) {
+ target.classList.toggle("hidden");
+ }
+}
+
+function AddClass(element, name) {
+ var current = element.className;
+ var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', 'g');
+ if (expression.test(current)) {
+ return;
+ }
+ element.className = current + ' ' + name;
+}
+
+function RemoveClass(element, name) {
+ var current = element.className;
+ var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', '');
+ var match = expression.exec(current);
+ while ((match = expression.exec(current)) != null) {
+ if (match[1].length > 0 && match[2].length > 0) {
+ current = current.substr(0, match.index + match[1].length)
+ + current.substr(match.index + match[0].length);
+ } else {
+ current = current.substr(0, match.index)
+ + current.substr(match.index + match[0].length);
+ }
+ }
+ element.className = current;
+}
+
+function EaseOutQuad(t) {
+ return t * t;
+}
+
+function EaseInOutQuad(t) {
+ return (t < 0.5) ? (2 * t * t) : ((4 - 2 * t) * t - 1);
+}
+
+function ScrollHeight() {
+ if ("innerHeight" in window) {
+ return window.innerHeight;
+ } else if (document.documentElement) {
+ return document.documentElement.clientHeight;
+ }
+ return document.body.clientHeight;
+}
+
+function ScrollTo(top, callback) {
+ var html = document.documentElement;
+ var current = html.scrollTop;
+ var delta = top - current;
+ var finish = function () {
+ html.scrollTop = top;
+ if (callback) {
+ callback();
+ }
+ };
+ if (!window.performance.now || delta == 0) {
+ finish();
+ return;
+ }
+ var transition = EaseOutQuad;
+ var max = 300;
+ if (delta < -max) {
+ current = top + max;
+ delta = -max;
+ } else if (delta > max) {
+ current = top - max;
+ delta = max;
+ } else {
+ transition = EaseInOutQuad;
+ }
+ var duration = 150;
+ var interval = 7;
+ var time = window.performance.now();
+ var animate = function () {
+ var now = window.performance.now();
+ if (now >= time + duration) {
+ finish();
+ return;
+ }
+ var dt = (now - time) / duration;
+ html.scrollTop = Math.round(current + delta * transition(dt));
+ setTimeout(animate, interval);
+ };
+ setTimeout(animate, interval);
+}
+
+function ScrollToElement(element, callback) {
+ var header = document.getElementsByClassName("page_header")[0];
+ var headerHeight = header.offsetHeight;
+ var html = document.documentElement;
+ var scrollHeight = ScrollHeight();
+ var available = scrollHeight - headerHeight;
+ var padding = 10;
+ var top = element.offsetTop;
+ var height = element.offsetHeight;
+ var desired = top
+ - Math.max((available - height) / 2, padding)
+ - headerHeight;
+ var scrollTopMax = html.offsetHeight - scrollHeight;
+ ScrollTo(Math.min(desired, scrollTopMax), callback);
+}
+
+function GoToMessage(messageId) {
+ var element = document.getElementById("message" + messageId);
+ if (element) {
+ var hash = "#go_to_message" + messageId;
+ if (location.hash != hash) {
+ location.hash = hash;
+ }
+ ScrollToElement(element, function () {
+ AddClass(element, "selected");
+ setTimeout(function () {
+ RemoveClass(element, "selected");
+ }, 1000);
+ });
+ } else {
+ ShowToast("This message was not exported. Maybe it was deleted.");
+ }
+ return false;
+}
+
+function GoBack(anchor) {
+ if (!window.AllowBackFromHistory) {
+ return true;
+ }
+ history.back();
+ if (!anchor || !anchor.getAttribute) {
+ return true;
+ }
+ var destination = anchor.getAttribute("href");
+ if (!destination) {
+ return true;
+ }
+ setTimeout(function () {
+ location.href = destination;
+ }, 100);
+ return false;
+}
diff --git a/ChatExport_2026-04-06/messages.html b/ChatExport_2026-04-06/messages.html
new file mode 100644
index 0000000..2e5fb2e
--- /dev/null
+++ b/ChatExport_2026-04-06/messages.html
@@ -0,0 +1,15558 @@
+
+
+
+
+
+
+Exported Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:08
+
+
+
+Рустам Брат Леськи
+
+
+
+Привет
+
+
+
+
+
+
+
+
+
+
+
+13:08
+
+
+
+Я в сервисе
+
+
+
+
+
+
+
+
+
+
+
+13:08
+
+
+
+Диагностику сделали двигателя
+
+
+
+
+
+
+
+
+
+
+
+13:08
+
+
+
+Задиры на всех
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:10
+
+
+
+Rustam Khalimov
+
+
+
+Привет, ну это печально, но не смертельно, кстати фотки проси, наебать могут
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:12
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+Марс 01.03.2026 12:48:20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:12
+
+
+
+Рустам Брат Леськи
+
+
+
+250-300к(
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:14
+
+
+
+Rustam Khalimov
+
+
+
+На вид не страшно вроде
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:10
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Созвон с руководителем сейчас прошел.
+
+
+
+
+
+
+
+
+
+
+
+22:10
+
+
+
+Ваши ставки, уволили или нет?
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Рустам Брат Леськи
+
+
+
+Нет конечно
+
+
+
+
+
+
+
+🤝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Rustam Khalimov
+
+
+
+Повысили, пока только на словах
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Рустам Брат Леськи
+
+
+
+Сказал наверно дальше дома сиди
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:12
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Он просил, что будет, если ЗП не дадут, я сказал, что похуй мне и буду дома сидеть
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:14
+
+
+
+Рустам Брат Леськи
+
+
+
+У меня только что просто так пару раз телефон перезагрузился
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:15
+
+
+
+Rustam Khalimov
+
+
+
+Да ёпрст, проблемы навалили
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:19
+
+
+
+Рустам Брат Леськи
+
+
+
+Да что-то совсем
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:09
+
+
+
+Рустам Брат Леськи
+
+
+
+Новый двигатель думаю буду лучше уж покупать. Пока недели 2-3 поезжу на этом если смогу обдумаю
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:09
+
+
+
+Rustam Khalimov
+
+
+
+Тебе решать
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:10
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:11
+
+
+
+Ну да. Много уж подумал, и драйв весь перечитал.
+
+
+
+
+
+
+
+
+
+
+
+01:11
+
+
+
+Меняют, нормально ездят
+
+
+
+
+
+
+
+
+
+
+
+01:11
+
+
+
+Новый двигатель получается. Но без навесного
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:11
+
+
+
+Rustam Khalimov
+
+
+
+Завтра гляну, сегодня уже спать лен, завтра рано вставать
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:12
+
+
+
+Рустам Брат Леськи
+
+
+
+Понял. Спокойной ночи
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:12
+
+
+
+Rustam Khalimov
+
+
+
+Сладких!
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:44
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Да проще часть нового купить, чем со старого ставить, за то в новом куча дерьма не вылезет как со старым
+
+
+
+
+
+
+
+
+
+
+
+08:45
+
+
+
+Считай меньше спецов надо, меньше шансов накосячить
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:45
+
+
+
+Рустам Брат Леськи
+
+
+
+Да, навесное не сильно и дорого. Да новое все
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:45
+
+
+
+Rustam Khalimov
+
+
+
+И если замена двигателя, то коробку тоже снимать будут и прокладки менять или герметик, получается и масло в ней
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:46
+
+
+
+Рустам Брат Леськи
+
+
+
+Свечи, модуль зажигания, термостат, помпа, ремень ГРМ, генератора
+
+
+
+
+
+
+
+
+
+
+
+08:46
+
+
+
+
+
+Вот это незнаю. Если коробку снимают, то как минимум масло поменять бы конечно
+
+
+
+
+
+
+
+
+
+
+
+08:47
+
+
+
+Но коробку новую не буду уж пока
+
+
+
+
+
+
+
+
+
+
+
+08:47
+
+
+
+Хотя смотрел вчера на Авито, за 80к можно вроде найти
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:47
+
+
+
+Rustam Khalimov
+
+
+
+Лучше сразу, там где течь поменять прокладку или герметик
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:48
+
+
+
+Рустам Брат Леськи
+
+
+
+Да, сцепление вроде можно поменять на новое
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:48
+
+
+
+Rustam Khalimov
+
+
+
+Главное в тачке кузов, благо с ним всё збс
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:49
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну да
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:49
+
+
+
+Rustam Khalimov
+
+
+
+Подушки тоже под замену будут
+
+
+
+
+
+
+
+
+
+
+
+08:49
+
+
+
+Двигателя
+
+
+
+
+
+
+
+
+
+
+
+08:51
+
+
+
+Если ЗП всю получу, то наверное смогу перезанять, что бы всё разом сделать
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:51
+
+
+
+Рустам Брат Леськи
+
+
+
+Точно
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:57
+
+
+
+Rustam Khalimov
+
+
+
+
+
+А там разве ГРМ не стоит уже новый?
+
+
+
+
+
+
+
+
+
+
+
+
+
+09:05
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Спасибо. Буду стараться своими силами уж. Пока я и время взял до месяца на обдумывание, остальное с кредитки, там главное в течение 100 дней вернуть, то не будет процентов
+
+
+
+
+
+
+
+
+
+
+
+09:05
+
+
+
+
+
+Вчера с сервисником общался он сказал вроде нет
+
+
+
+
+
+
+
+
+
+
+
+09:06
+
+
+
+На Авито продавцу вопрос задал, пока не отвечает
+
+
+
+
+
+
+
+
+
+
+
+09:07
+
+
+
+Ремни ГРМ и модуль зажигания есть на новом, посмотрел оказывается ответил
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:39
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:39
+
+
+
+
+
+Вот что вышло
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:40
+
+
+
+Рустам Брат Леськи
+
+
+
+😄
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:56
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:57
+
+
+
+Рустам Брат Леськи
+
+
+
+Фигасе
+
+
+
+
+
+
+
+
+
+
+
+11:58
+
+
+
+Страшилка
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:58
+
+
+
+Rustam Khalimov
+
+
+
+Да, четко выходит!
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:45
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:47
+
+
+
+Rustam Khalimov
+
+
+
+Ого, а другие есть?
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:48
+
+
+
+Рустам Брат Леськи
+
+
+
+Не
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:49
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:50
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+23:58
+
+
+
+Rustam Khalimov
+
+
+
+Ща сделаем
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:00
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:00
+
+
+
+Сегодня обнаружили что вот эта фигня не застегнутая была
+
+
+
+
+
+
+
+
+
+
+
+00:01
+
+
+
+Что-то машина подозрительно лучше начала работать
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:01
+
+
+
+Rustam Khalimov
+
+
+
+Хм, интересно уже
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:01
+
+
+
+Рустам Брат Леськи
+
+
+
+Но может кажется
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:01
+
+
+
+Rustam Khalimov
+
+
+
+И это никто не заметил
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:01
+
+
+
+Рустам Брат Леськи
+
+
+
+Завтра утром узнаю
+
+
+
+
+
+
+
+
+
+
+
+00:02
+
+
+
+Сегодня еле с утра завели
+
+
+
+
+
+
+
+
+
+
+
+00:02
+
+
+
+
+
+Да она сзади же
+
+
+
+
+
+
+
+
+
+
+
+00:02
+
+
+
+Форсунки промыли ещё
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:03
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Компрессию мерили?
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:03
+
+
+
+Рустам Брат Леськи
+
+
+
+Не
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:03
+
+
+
+Rustam Khalimov
+
+
+
+Ебать они дауны тогда, я бы не доверял им
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:04
+
+
+
+Рустам Брат Леськи
+
+
+
+Почему-то все сказали что это ничего не даст
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:04
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Да как так то, если есть задиры. должна и декомпрессия быть
+
+
+
+
+
+
+
+
+
+
+
+00:05
+
+
+
+Хотя я не прав
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:05
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну вот они и сказали что там полюбому декомпрессия и не мерили. Я что-то сам не подумал понастоять
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:07
+
+
+
+Rustam Khalimov
+
+
+
+Но проверить стоило, у меня на четырке была компрессия 12-12-8-12 по цилиндрам. В итоге там где 8 прогарел выпускной клапан, а так бы вместо замены клапана капиталка всего двигателя была бы
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:09
+
+
+
+Рустам Брат Леськи
+
+
+
+Понял. Но у меня то все равно задиры
+
+
+
+
+
+
+
+
+
+
+
+00:09
+
+
+
+
+
+Пока я пытаюсь понять что это
+
+
+
+
+
+
+
+
+
+
+
+00:10
+
+
+
+У меня расход же ещё 13-14
+
+
+
+
+
+
+
+
+
+
+
+00:10
+
+
+
+Может сказали как раз изза этого
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:11
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:13
+
+
+
+Зря ты gemini не используешь
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:13
+
+
+
+Рустам Брат Леськи
+
+
+
+Фигасе это что так нейросеть работает
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:13
+
+
+
+
+
+Походу да очень
+
+
+
+
+
+
+
+
+
+
+
+00:14
+
+
+
+Я очень удивлен
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:15
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:16
+
+
+
+Что то похоже не в двигателе проблема
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:17
+
+
+
+Рустам Брат Леськи
+
+
+
+Походу да
+
+
+
+
+
+
+
+
+
+
+
+00:17
+
+
+
+Жду очень утра
+
+
+
+
+
+
+
+
+
+
+
+00:18
+
+
+
+Крутяк очень оказывается гемини
+
+
+
+
+
+
+
+
+
+
+
+00:18
+
+
+
+Но с телефона бы конечно ещё
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:20
+
+
+
+Rustam Khalimov
+
+
+
+С телефона так себе, с компа норм
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:23
+
+
+
+Короче выдыхай, двигатель живой и это норма
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:24
+
+
+
+Рустам Брат Леськи
+
+
+
+Пиздец ии очень умный
+
+
+
+
+
+
+
+
+
+
+
+00:24
+
+
+
+И все так разлагает
+
+
+
+
+
+
+
+
+
+
+
+00:24
+
+
+
+
+
+Да, я выдыхнул
+
+
+
+
+
+
+
+
+
+
+
+00:25
+
+
+
+А эти черные нагары на клапане не надо ли убрать?
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:25
+
+
+
+Rustam Khalimov
+
+
+
+Пора сервис открывать, я трактор через него чинил, он мне говорил как полуось сварить, что бы ее снова не сломало. Карбюратор через нее настроил по фото. Навес все расчеты по нагрузке тоже
+
+
+
+
+
+
+
+
+
+
+
+00:25
+
+
+
+
+
+Сейчас узнаем
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:25
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+И не говори. Все можно
+
+
+
+
+
+
+
+
+
+
+
+00:26
+
+
+
+У меня к гемини потом ещё один вопрос есть. Не по машине
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:28
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:31
+
+
+
+Рустам Брат Леськи
+
+
+
+Итальянскую прожарку надо сделать
+
+
+
+
+
+
+
+
+
+
+
+00:32
+
+
+
+У меня эндостоп есть же ещё свой. Потом как нибудь проверить
+
+
+
+
+
+
+
+
+
+
+
+00:32
+
+
+
+На выходных в Челны собираемся съездить, потом посмотрю
+
+
+
+
+
+
+
+
+
+
+
+00:32
+
+
+
+Может реально и уйдет
+
+
+
+
+
+
+
+
+
+
+
+00:32
+
+
+
+Это может вот в последние дни собралось
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:33
+
+
+
+Rustam Khalimov
+
+
+
+Благо на опелях/шевроле это легко снять свечи, у меня и на шкоде без спец инструмента невозможно
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:33
+
+
+
+Рустам Брат Леськи
+
+
+
+А то и свечи которые 4 дня назад меняли все в грязи уже
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:33
+
+
+
+Rustam Khalimov
+
+
+
+Трубку поменяй, она не дорогая и сам можешь или ко мне приедь, сменим
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:33
+
+
+
+Рустам Брат Леськи
+
+
+
+Да у меня просто там очень
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:34
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Так конечно смесь бедная судя по всему, вот и свечи заливает и нагар от этого
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:34
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Ну да. Но я думаю она целая наверно. Просто незастегнутая была
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:34
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Лучше поменяй
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:34
+
+
+
+Рустам Брат Леськи
+
+
+
+Мастер как полез ща за форсунками сразу сам удивился
+
+
+
+
+
+
+
+
+
+
+
+00:34
+
+
+
+
+
+Хорошо
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:36
+
+
+
+Rustam Khalimov
+
+
+
+General Motors (GM): 55559239 (это самый распространенный оригинальный номер). Opel: 5607496 или 05607496. На что обратить внимание при покупке Оригинал против аналогов: Оригинальная трубка пластиковая и довольно дорогая для обычного куска пластика. Существуют аналоги (например, фирмы Dominant, ZZVF или Patron), они дешевле, но пластик там бывает еще более хрупким. Уплотнительные кольца: Внутри защелок на концах трубки должны стоять резиновые колечки. Если берете дешевый аналог, проверьте их наличие, иначе подсос воздуха останется даже с новой трубкой. Альтернатива (колхозинг): Многие владельцы вместо покупки дорогого пластика покупают обычный маслобензостойкий шланг (внутренний диаметр обычно 10-12 мм, нужно уточнить по месту) и два хороших винтовых хомута. Это надежнее, так как резина не треснет от вибраций и жара, как штатный пластик. Как ее заменить самостоятельно Там стоят быстросъемные соединения. Чтобы снять старую трубку, нужно: Сжать стопорное кольцо (ушки) на пластиковом наконечнике. Аккуратно потянуть на себя. Осторожно: Если трубка старая, она, скорее всего, просто рассыплется у вас в руках — будьте готовы к тому, что осколки пластика могут упасть за двигатель. Учитывая, что вы из Казани, эту деталь легко найти в наличии в крупных магазинах запчастей или заказать с доставкой за 1-2 дня.
+
+
+
+
+
+
+
+
+
+
+
+00:36
+
+
+
+
+
+Что там?
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:37
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:37
+
+
+
+Что за красная кнопка?
+
+
+
+
+
+
+
+
+
+
+
+00:37
+
+
+
+Фотка конечно с плохого ракурса
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:38
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:38
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:39
+
+
+
+
+
+Пиздец
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:39
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Ищи по другим номерам
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:39
+
+
+
+Рустам Брат Леськи
+
+
+
+Мы эту информацию несколько часов искали
+
+
+
+
+
+
+
+😁
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:40
+
+
+
+Ну я знал что это перевод на ручной. Но именно принцип хотел точный узнать, а он нигде не расписан
+
+
+
+
+
+
+
+
+
+
+
+00:40
+
+
+
+А проверить никак. Там пломбы стоят
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:40
+
+
+
+Rustam Khalimov
+
+
+
+Я сейчас всякую хуйню, в которой сомневаюсь или не пониаю, через ИИ прогоняю и сразу всё ясно
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:41
+
+
+
+Рустам Брат Леськи
+
+
+
+Тоже надо начинать уже
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:41
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Вот, пора использовать все знания нейронок
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:41
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну да
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:42
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Главное логически хотя бы перепроверять, а то не всегда там то, что нужно
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:45
+
+
+
+Рустам Брат Леськи
+
+
+
+Можно к Ильдару автоподбор на оживление идти помогать
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:46
+
+
+
+Rustam Khalimov
+
+
+
+Можно, но зачем? Я думаю, он сам уже юзает что то подобное
+
+
+
+
+
+
+
+
+
+
+
+00:47
+
+
+
+Я смотрел на ютубе как древний уазик с ии запускают, который 7 лет стоял
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:48
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Ничего себе
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:49
+
+
+
+Rustam Khalimov
+
+
+
+Ладно, спать пора! Я на покой ушел! Заранее сладких!
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:49
+
+
+
+Рустам Брат Леськи
+
+
+
+Сладких снов
+
+
+
+
+
+
+
+
+
+
+
+00:49
+
+
+
+Я очень утра жду
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:49
+
+
+
+Rustam Khalimov
+
+
+
+Зачем утра, что бы патрубок воткнуть?
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:49
+
+
+
+Рустам Брат Леськи
+
+
+
+Патрубок то воткнули уже
+
+
+
+
+
+
+
+
+
+
+
+00:50
+
+
+
+Завести попробовать на холодную
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:50
+
+
+
+Rustam Khalimov
+
+
+
+А, ты после не пробывал?
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:50
+
+
+
+Рустам Брат Леськи
+
+
+
+Пробовал. Но обычно только по утрам проблема
+
+
+
+
+
+
+
+
+
+
+
+00:51
+
+
+
+4-5 часов обычно не влияют
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:51
+
+
+
+Rustam Khalimov
+
+
+
+Ну значит завтра момент истины, хотя, если свечи залиты, то хз, повлияет ли
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:51
+
+
+
+Рустам Брат Леськи
+
+
+
+А вот поэтому то мне и говорили. Что когда прогревается цилиндры расширяются и уже лучше становится
+
+
+
+
+
+
+
+
+
+
+
+00:52
+
+
+
+А на холодном из-за задиров не может запуститься
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:52
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Ну если так, то там зазор в цилиндрах должен быть 0,1 мм и больше
+
+
+
+
+
+
+
+
+
+
+
+00:53
+
+
+
+
+
+А по фото это не так
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:53
+
+
+
+Рустам Брат Леськи
+
+
+
+Буду надеяться из-за патрубка
+
+
+
+
+
+
+
+
+
+
+
+00:53
+
+
+
+Жду утра
+
+
+
+
+
+
+
+
+
+
+
+00:53
+
+
+
+Споки
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:53
+
+
+
+Rustam Khalimov
+
+
+
+Там бы стук был явный на холодную, даже не стук, а вибрация
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:54
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Наверно
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:54
+
+
+
+Rustam Khalimov
+
+
+
+Давай, до утра, отпишись. Но я завтра в 12 только встану.
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:54
+
+
+
+Рустам Брат Леськи
+
+
+
+Хорошо
+
+
+
+
+
+
+
+
+
+
+
+00:55
+
+
+
+И сервис просил написать с утра. Тоже ждут информацию
+
+
+
+
+
+
+
+
+
+
+
+
+
+08:37
+
+
+
+Рустам Брат Леськи
+
+
+
+Еле завел машину
+
+
+
+
+
+
+
+
+
+
+
+
+
+09:13
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:38
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+АвтоСеть ГАРАЖ АртСити 03.03.2026 09:42:23
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:38
+
+
+
+
+
+
+
+АвтоСеть ГАРАЖ АртСити 03.03.2026 10:06:04
+
+
+
+150000руб мотор с документами для регистрации. Доставка 2-3 дня. 100% предоплата
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:38
+
+
+
+
+
+Замена + мотор + расходники выйдет около 220000руб. Капитальный ремонт выйдет около 2500000, но точная сумма появится только после деффектовки вашего мотора (разборки)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:45
+
+
+
+Rustam Khalimov
+
+
+
+Да уж, в итоге ничего так и не понятно, что делать, они тебе говорят всё менять, но логики в этом нет
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:45
+
+
+
+Рустам Брат Леськи
+
+
+
+Да вот именно
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:46
+
+
+
+Rustam Khalimov
+
+
+
+Леське так же говорили в гараже, что капитала нужна, оказалось, что турбина
+
+
+
+
+
+
+
+
+
+
+
+13:46
+
+
+
+Так бы встряли на 250к и проблема бы осталась
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:47
+
+
+
+Рустам Брат Леськи
+
+
+
+Да я и вот думаю вдруг проблема останется
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:48
+
+
+
+Rustam Khalimov
+
+
+
+Делема
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:03
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:09
+
+
+
+Rustam Khalimov
+
+
+
+Диагностика нормальная нужна, каждый датчик прозванивать и прочее
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:11
+
+
+
+Рустам Брат Леськи
+
+
+
+Да. К другим надо съездить показать
+
+
+
+
+
+
+
+
+
+
+
+14:11
+
+
+
+Сегодня хочу вот этот датчик температуры на термостате поменять сам
+
+
+
+
+
+
+
+
+
+
+
+14:12
+
+
+
+Быстро там оказывается. Просто фишка
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:12
+
+
+
+Rustam Khalimov
+
+
+
+Можно конечно
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:12
+
+
+
+Рустам Брат Леськи
+
+
+
+Он мастер и говорил что в термостате у меня проблема
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:14
+
+
+
+Rustam Khalimov
+
+
+
+С этим ладно, а капиталка тут причем, из-за задиров и всё, которые не влияет ни на что в твоем случае?
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:19
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну да очень странно. Тут просто завести нужно же. В основном поршневая группа рабочая же
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:37
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:39
+
+
+
+Rustam Khalimov
+
+
+
+Заезжай ближе к вечеру, глянем что там, промоем, датчик поменяем сразу. Свечи проверим.
+
+
+
+
+
+
+
+
+
+
+
+14:39
+
+
+
+Только обезжиривателя у меня нет
+
+
+
+
+
+
+
+
+
+
+
+14:40
+
+
+
+Купи в балоне, как спрей
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:42
+
+
+
+Рустам Брат Леськи
+
+
+
+Хорошо. Датчик и обезжириватель куплю
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:14
+
+
+
+Рустам Брат Леськи
+
+
+
+Походу точно в датчике охлаждающей жидкости проблема
+
+
+
+
+
+
+
+
+
+
+
+15:15
+
+
+
+Что-то до этого Ютуб не открывал, а тут решил. Все подряд на этот датчик валят
+
+
+
+
+
+
+
+
+
+
+
+15:15
+
+
+
+И симптомы очень схожие с моими
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:17
+
+
+
+Rustam Khalimov
+
+
+
+Возможно
+
+
+
+
+
+
+
+
+
+
+
+
+
+16:47
+
+
+
+Рустам Брат Леськи
+
+
+
+Еду за датчиком в росско
+
+
+
+
+
+
+
+
+
+
+
+16:47
+
+
+
+В Биби не оказалось
+
+
+
+
+
+
+
+
+
+
+
+
+
+16:47
+
+
+
+Rustam Khalimov
+
+
+
+Ок
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+16:49
+
+
+
+Рустам Брат Леськи
+
+
+
+Много снега очень
+
+
+
+
+
+
+
+
+
+
+
+
+
+16:58
+
+
+
+Rustam Khalimov
+
+
+
+Да
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+09:51
+
+
+
+Рустам Брат Леськи
+
+
+
+Ураааа
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:35
+
+
+
+Рустам Брат Леськи
+
+
+
+С пол пинка прям завелся
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:39
+
+
+
+Rustam Khalimov
+
+
+
+Это же супер
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:41
+
+
+
+Рустам Брат Леськи
+
+
+
+Очень супер
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:26
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+ГЕНЕРАЛЬНЫЙ ШТАБ 04.03.2026 13:25:10
+
+
+
+
+
+Израиль опубликовал кадры "уничтожения иранского военного вертолёта", но это было просто изображение, нарисованное на земле😂
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:27
+
+
+
+Рустам Брат Леськи
+
+
+
+Зато точно в цель
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:28
+
+
+
+Rustam Khalimov
+
+
+
+Капиталку будешь делать, ну хотя бы просто так, что бы 200к просрать?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:29
+
+
+
+Рустам Брат Леськи
+
+
+
+Конечно
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+Чтобы не просто несколько в тревоге быть, надо уж до конца довести
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+Сегодня масло поеду менять
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+К ним же
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+Rustam Khalimov
+
+
+
+Как они отреагировали то?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+Рустам Брат Леськи
+
+
+
+И ходовую осмотр для начала
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+
+
+Не прочитали даже
+
+
+
+
+
+
+
+
+
+
+
+13:30
+
+
+
+С утра написал
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:31
+
+
+
+Rustam Khalimov
+
+
+
+Я бы им не доверял
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:31
+
+
+
+Рустам Брат Леськи
+
+
+
+Да. Но блин удобное расположение для меня
+
+
+
+
+
+
+
+
+
+
+
+13:31
+
+
+
+Я ещё это здание обслуживать сегодня поеду. Где они арендуют
+
+
+
+
+
+
+
+
+
+
+
+13:32
+
+
+
+Масло уж поменяют
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:33
+
+
+
+Rustam Khalimov
+
+
+
+Понимаю, да, надеюсь всё нормально сделают
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:33
+
+
+
+Рустам Брат Леськи
+
+
+
+Да
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:37
+
+
+
+Rustam Khalimov
+
+
+
+Привет, про кредит не забудь
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:25
+
+
+
+Рустам Брат Леськи
+
+
+
+Привет. Скинул 40к только. Положить наличку не успел сегодня. Завтра ещё 10к скину
+
+
+
+
+
+
+
+👌
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:01
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:01
+
+
+
+Решил ноутбук почистить
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:06
+
+
+
+Rustam Khalimov
+
+
+
+Пипец
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:07
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:13
+
+
+
+Фуух
+
+
+
+
+
+
+
+
+
+
+
+18:14
+
+
+
+Собрал. Монитор не запускался, несколько раз шлейф монитора первтыкал проверял никак
+
+
+
+
+
+
+
+
+
+
+
+18:14
+
+
+
+Обратно все разобрал, походу оперативка не до конца была засунута
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:35
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:35
+
+
+
+
+
+Это Алсу ноутбук
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:35
+
+
+
+Rustam Khalimov
+
+
+
+Он ещё и на жёстком диске
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:36
+
+
+
+Рустам Брат Леськи
+
+
+
+Да. Как нибудь на ссд переделать надо будет
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:47
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:48
+
+
+
+Рустам Брат Леськи
+
+
+
+Немного конечно осталось
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:48
+
+
+
+На два перевода уж только смогу
+
+
+
+
+
+
+
+
+
+
+
+13:49
+
+
+
+Я заказал ххд в озоне вчера вс таки
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Только странно недорого
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Rustam Khalimov
+
+
+
+Круто, надеюсь нормальный
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Рустам Брат Леськи
+
+
+
+Western Digital 2 ТБ Внутренний жесткий диск (WD20EFRX)
https://ozon.ru/t/DurWSTT
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Rustam Khalimov
+
+
+
+Когда придет твой NAS?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:51
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+С 14 по 22 стоит
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:51
+
+
+
+Rustam Khalimov
+
+
+
+Неплохо
+
+
+
+
+
+
+
+
+
+
+
+13:52
+
+
+
+
+
+Реально дёшево
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Ну чё то. Может кирпич просто придет
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+В озоне прямо посмотрю по виду как придёт
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:02
+
+
+
+Rustam Khalimov
+
+
+
+И проверить дома нароботку и битые сектора
+
+
+
+
+
+
+
+
+
+
+
+14:03
+
+
+
+Хотя по отзывам б/у прошитый
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:05
+
+
+
+Я бы отказался сразу
+
+
+
+
+
+
+
+
+
+
+
+14:05
+
+
+
+Даже ждать смысла нет
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:05
+
+
+
+Рустам Брат Леськи
+
+
+
+Да согласен
+
+
+
+
+
+
+
+
+
+
+
+14:05
+
+
+
+Сейчас
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:07
+
+
+
+Rustam Khalimov
+
+
+
+За эти деньги лучше возьми ssd nvme на 512
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:14
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну да. Но это все nas ради ххд в основном делается же
+
+
+
+
+
+
+
+
+
+
+
+14:15
+
+
+
+Поэтому его хотел вначале
+
+
+
+
+
+
+
+
+
+
+
+14:15
+
+
+
+Или nvme тоже в райд можно встроить?🤔
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:16
+
+
+
+Rustam Khalimov
+
+
+
+Можно, но их 2 надо одинаковых
+
+
+
+
+
+
+
+
+
+
+
+14:16
+
+
+
+Как и HDD
+
+
+
+
+
+
+
+
+
+
+
+14:21
+
+
+
+Я что то прям вовремя заморочился, сейчас бы мой сервер мне в 400-500к обошелся вместо 150
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:21
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну да
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:22
+
+
+
+Мне бы тоже пол года назад надо было
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:22
+
+
+
+Rustam Khalimov
+
+
+
+Может тебе его продать, я про диск, и купить 2 подходящих
+
+
+
+
+
+
+
+
+
+
+
+14:23
+
+
+
+Можно и б/у взять под рейд, года 2022 и выше
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:24
+
+
+
+Рустам Брат Леськи
+
+
+
+Да может. Но и подходящий же стоят очень дорого
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:24
+
+
+
+Rustam Khalimov
+
+
+
+Но ты можешь сделать иначе, поставить тот что у тебя есть пока что, потом купить на 8тб и туда бэкапы делать
+
+
+
+
+
+
+
+
+
+
+
+14:25
+
+
+
+Потом второй купить на 8 и их оба в рейд, а этот на 2 сольёшь
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:26
+
+
+
+Рустам Брат Леськи
+
+
+
+Я просто боюсь вот я пока буду копить файлы все туда. А потом когда куплю новый поставлю. Тогда когда рейд буду делать, вдруг что-то пойдет не так и все стерется
+
+
+
+
+
+
+
+
+
+
+
+14:26
+
+
+
+8 много уж
+
+
+
+
+
+
+
+
+
+
+
+14:26
+
+
+
+4 точно хватит
+
+
+
+
+
+
+
+
+
+
+
+14:26
+
+
+
+Я уже думаю и 2 хватит
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:27
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Ну если изначально криво сделать, то да, пойдет всё не так, когда рэйд сделаешь, можно не ту схему выбрать и копирнуть пустой на полный, а не наоборот
+
+
+
+
+
+
+
+
+
+
+
+14:28
+
+
+
+
+
+Тебе решать, мне пока 2 хватает, но у меня всё хитрее сделано
+
+
+
+
+
+
+
+
+
+
+
+14:29
+
+
+
+У меня зеркало на 8ТБ поделенных посекторно на блоки по 2ТБ
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:29
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Ну вот поэтому я хотел на 2тб сразу запустить
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:30
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Не расчитывай, что из коробки сразу всё огонь будет, может не понравится тебе заводская ос и сразу будешь норм делать, пару раз снесешь систему, сразу смысла делать нет, 2-4 недели потестить надо
+
+
+
+
+
+
+
+
+
+
+
+14:32
+
+
+
+Я могу тебе б/у 2 тошибы дать, пока не купишь нормальные, они верой и правдой служили, там потом по одному заменишь и мне вернешь
+
+
+
+
+
+
+
+
+
+
+
+14:33
+
+
+
+На них потренируемся, посмотрим как работает зеркало, если один убрать и другой воткнуть
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:37
+
+
+
+Этот попизже будет 19к за 6ТБ, чем 12к за 2ТБ
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:49
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Ну да уж. Но это только один же ещё
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:55
+
+
+
+Rustam Khalimov
+
+
+
+За то на годы хватит. Короче надо вживую общаться и думать
+
+
+
+
+
+
+
+
+
+
+
+14:56
+
+
+
+Моё предложение, как всё придет, собраться у меня, поставить б/у диски, простестить ОС как оно работает и что даёт. Дальше думать будем.
+
+
+
+
+
+
+
+
+
+
+
+14:56
+
+
+
+Ну и оперативку докинем
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:05
+
+
+
+Рустам Брат Леськи
+
+
+
+Да согласен, лучше вживую вместе потестить посмотреть
+
+
+
+
+
+
+
+🤝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:56
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+Rustam Khalimov 13.03.2026 11:56:09
+
+
+
+
+
+Пизда вам ресурсы, от локальной ИИ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+16:02
+
+
+
+Рустам Брат Леськи
+
+
+
+Ничего себе
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+Rustam Khalimov 14.03.2026 22:10:02
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+
+
+Как тебе дизайн?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Rustam Khalimov
+
+
+
+Я месседжер пишу свой, что бы локально развернуть
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:13
+
+
+
+Рустам Брат Леськи
+
+
+
+Прикольно
+
+
+
+
+
+
+
+
+
+
+
+22:13
+
+
+
+Когда готов будет скажешь
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:14
+
+
+
+Rustam Khalimov
+
+
+
+Обязательно, я же конфидерацию хочу сделать, что бы люди сами на свои сервера ставили и можно было с другими серверами общаться
+
+
+
+
+
+
+
+
+
+
+
+22:14
+
+
+
+Видео и аудио звонки уже сделал и конференции на 200 человек
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:12
+
+
+
+Rustam Khalimov
+
+
+
+Куда едите?
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:12
+
+
+
+Рустам Брат Леськи
+
+
+
+Никуда, только проснулись
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:13
+
+
+
+Rustam Khalimov
+
+
+
+А, я думал это вы, увидели на дороги вашу копию
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:13
+
+
+
+Рустам Брат Леськи
+
+
+
+Аа
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:57
+
+
+
+Рустам Брат Леськи
+
+
+
+А вы куда ездили?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:57
+
+
+
+Rustam Khalimov
+
+
+
+Порт
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:58
+
+
+
+Рустам Брат Леськи
+
+
+
+Мы вот только выходить будем из дома
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:58
+
+
+
+Rustam Khalimov
+
+
+
+Понял, NAS пришёл?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:58
+
+
+
+Рустам Брат Леськи
+
+
+
+Ещё нет
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:58
+
+
+
+Rustam Khalimov
+
+
+
+Кстати приезжайте, купаты приготовим
+
+
+
+
+
+
+
+
+
+
+
+13:59
+
+
+
+И бампер поможешь мне скинуть
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:59
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+Rustam Khalimov
+
+
+
+Если конечно у вас других планов нет
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Планы были конечно(
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+Rustam Khalimov
+
+
+
+Ничего страшного
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:00
+
+
+
+Рустам Брат Леськи
+
+
+
+Так то приехали бы
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:52
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:09
+
+
+
+Рустам Брат Леськи
+
+
+
+А что это когда? Давно было?
+
+
+
+
+
+
+
+
+
+
+
+15:10
+
+
+
+А из чего эта платина? Металл или пластик?
+
+
+
+
+
+
+
+
+
+
+
+
+
+15:15
+
+
+
+Rustam Khalimov
+
+
+
+Пластик, об сугроб уебался при парковке
+
+
+
+
+
+
+
+
+
+
+
+15:15
+
+
+
+На той неделе
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:03
+
+
+
+Рустам Брат Леськи
+
+
+
+Жалко конечно. Крыло очень
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:03
+
+
+
+Rustam Khalimov
+
+
+
+
+
+До
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:03
+
+
+
+
+
+После
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:03
+
+
+
+Хули, шпаклевать и красить надо теперь
+
+
+
+
+
+
+
+
+
+
+
+17:04
+
+
+
+Выпрямил, уже збс
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:52
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:52
+
+
+
+Завтра ДР у нее, как думаешь, понравится ей?
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:48
+
+
+
+Rustam Khalimov
+
+
+
+
+
+И вот второй вариант
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:01
+
+
+
+Рустам Брат Леськи
+
+
+
+Привет. Недавно прослушал только, очень круто получились оба
+
+
+
+
+
+
+
+
+
+
+
+21:01
+
+
+
+Прикольно и красиво
+
+
+
+
+
+
+
+
+
+
+
+21:02
+
+
+
+Я Чебоксарах застрял на объекте. Сегодня на вторую ночь остаёмся
+
+
+
+
+
+
+
+
+
+
+
+21:02
+
+
+
+Ещё на работе нахожусь
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:02
+
+
+
+Rustam Khalimov
+
+
+
+Эх, надеюсь завтра обратно
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:03
+
+
+
+Рустам Брат Леськи
+
+
+
+Да надеюсь
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:23
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Это что за вечеруха?
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Рустам Брат Леськи
+
+
+
+Да вот надо все написать
+
+
+
+
+
+
+
+
+
+
+
+
+
+22:11
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+10:23
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+10:25
+
+
+
+И решился заказать 2шт wd red plus 4tb по 17к на Яндекс маркете
+
+
+
+
+
+
+
+
+
+
+
+10:25
+
+
+
+29-30 марта жесткие должны прийти
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:51
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:55
+
+
+
+Антифриз отсюда вытекал немного. Кольцо резиновое уплотнительное задубело
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:57
+
+
+
+Купил оригинальное с экзиста, вчера за минуту поменял. Оказывается все просто
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:07
+
+
+
+Rustam Khalimov
+
+
+
+Ну супер, круто
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:34
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:34
+
+
+
+Ураа
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:34
+
+
+
+Rustam Khalimov
+
+
+
+О, збс
+
+
+
+
+
+
+
+
+
+
+
+19:34
+
+
+
+Диски тоже пришли?
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:35
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Нет ещё
+
+
+
+
+
+
+
+
+
+
+
+19:35
+
+
+
+Это на 2тб который был пока вставил просто
+
+
+
+
+
+
+
+
+
+
+
+19:35
+
+
+
+Я только с работы пришел
+
+
+
+
+
+
+
+
+
+
+
+19:35
+
+
+
+Ещё даже не раздевался, сразу посмотрел😄
+
+
+
+
+
+
+
+
+
+
+
+19:36
+
+
+
+Попозже через часик включу посмотрю
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:36
+
+
+
+Rustam Khalimov
+
+
+
+Ага, ну хорошо
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:36
+
+
+
+Рустам Брат Леськи
+
+
+
+На выходные к тебе
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:36
+
+
+
+Rustam Khalimov
+
+
+
+Да, вместе глянем
+
+
+
+
+
+
+
+
+
+
+
+19:37
+
+
+
+У меня пока жопа полная, к собеседованию готовлюсь в озон, послезавтра уже, ебать будут как никогда
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:37
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+((
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:38
+
+
+
+Rustam Khalimov
+
+
+
+Первый этап прошел, второй надо, потом ещё один и ещё один, 4 этапа общее
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:38
+
+
+
+Рустам Брат Леськи
+
+
+
+Ничего себе
+
+
+
+
+
+
+
+
+
+
+
+19:38
+
+
+
+Удачи уж
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:39
+
+
+
+Rustam Khalimov
+
+
+
+400к попросил у них, согласились пока что, после собесов могут урезать
+
+
+
+
+
+
+
+
+
+
+
+19:39
+
+
+
+Спасибо
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:40
+
+
+
+Рустам Брат Леськи
+
+
+
+А с той работой все?
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:41
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:42
+
+
+
+Нихуя не понятно, тянут, а запасы нашы иссякли почти
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:46
+
+
+
+Рустам Брат Леськи
+
+
+
+Да плохо очень конечно что тянут и не говорят ничего
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+10:25
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+Антон Шабашов 24.03.2026 21:05:28
+
+
+
+
+
+
😂 Коротко о ситуации в на Ближнем Востоке
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:29
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Во наша статистика на двоих за год
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:42
+
+
+
+Рустам Брат Леськи
+
+
+
+Нормально
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:03
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:04
+
+
+
+Rustam Khalimov
+
+
+
+А ты уже затестил?
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:15
+
+
+
+Рустам Брат Леськи
+
+
+
+Да, я подключил так простенько пока
+
+
+
+
+
+
+
+
+
+
+
+12:15
+
+
+
+И просто загрузил файлы в диск
+
+
+
+
+
+
+
+
+
+
+
+12:15
+
+
+
+Бекап ещё не настроил
+
+
+
+
+
+
+
+
+
+
+
+12:15
+
+
+
+Но там оказывается все просто
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+В выходные если время будет посмотрим вместе ещё
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+Может и диски прийти успеют
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:17
+
+
+
+Rustam Khalimov
+
+
+
+Там много чего надо делать
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:18
+
+
+
+Рустам Брат Леськи
+
+
+
+Вначале там мастер помогает рейд настроить просто
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:19
+
+
+
+Rustam Khalimov
+
+
+
+Приложение для фоток как тебе или не тестил?
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:24
+
+
+
+Рустам Брат Леськи
+
+
+
+Не тестил ещё
+
+
+
+
+
+
+
+
+
+
+
+12:24
+
+
+
+Времени очень мало, поздно домой приезжаю
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+Rustam Khalimov
+
+
+
+Да, печально
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:21
+
+
+
+Rustam Khalimov
+
+
+
+Ты завтра планируешь?
+
+
+
+
+
+
+
+
+
+
+
+12:21
+
+
+
+Мы сегодня на др
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:28
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Да наверно. Точно не сегодня
+
+
+
+
+
+
+
+
+
+
+
+12:29
+
+
+
+Там жесткие пришли, с пвз сегодня заберу
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+14:07
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:13
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Во по кайфу теперь
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:15
+
+
+
+Рустам Брат Леськи
+
+
+
+Да, поставил старый на форматирование пока. 4 часа написалось
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:15
+
+
+
+Завтра думаю колеса на летние поменять
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:17
+
+
+
+Rustam Khalimov
+
+
+
+Го
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:40
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+01:40
+
+
+
+Оформляю возврат
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:52
+
+
+
+Rustam Khalimov
+
+
+
+Диски?
+
+
+
+
+
+
+
+
+
+
+
+
+
+11:57
+
+
+
+Рустам Брат Леськи
+
+
+
+Да
+
+
+
+
+
+
+
+
+
+
+
+11:57
+
+
+
+Только воткнул один запищал
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:02
+
+
+
+Rustam Khalimov
+
+
+
+Скорее всего диски тоже наебка б/у, сейчас много такого.
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:20
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:21
+
+
+
+Ну походу да бу
+
+
+
+
+
+
+
+
+
+
+
+12:21
+
+
+
+Пока оформил. Жду ответа от продавца
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:24
+
+
+
+Rustam Khalimov
+
+
+
+Оба на возврат или один?
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:24
+
+
+
+Рустам Брат Леськи
+
+
+
+Оба сделаю уж
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:25
+
+
+
+Rustam Khalimov
+
+
+
+Ну и правильно, лучше в Ситилинк взять через озон
+
+
+
+
+
+
+
+
+
+
+
+12:29
+
+
+
+Ты пробивал их по серийному номеру на сайте производителя
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:29
+
+
+
+Рустам Брат Леськи
+
+
+
+Не. Я где-то читал что не обязательно пробивается
+
+
+
+
+
+
+
+
+
+
+
+12:29
+
+
+
+Поэтому не стал
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+
+
+Ну да, так и сделаю наверно
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Обязательно, не может такого быть
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+Рустам Брат Леськи
+
+
+
+Главное чтобы эти продавец взял обратно
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+Что нибудь ещё напишет
+
+
+
+
+
+
+
+💯
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:53
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:53
+
+
+
+Это точно подделка
+
+
+
+
+
+
+
+
+
+
+
+12:53
+
+
+
+Хотя на фото разные
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:58
+
+
+
+Рустам Брат Леськи
+
+
+
+Один не просто скрипит, а совсем оказывается не работает
+
+
+
+
+
+
+
+😢
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:59
+
+
+
+При включении скрипит пытается. Потом все
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Rustam Khalimov
+
+
+
+Если местами менять то так же?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Рустам Брат Леськи
+
+
+
+Ну попал конечно
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+
+
+Не пробовал
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Посмотрю
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Я привезу все
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Rustam Khalimov
+
+
+
+Ок
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Когда ждать?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:00
+
+
+
+Рустам Брат Леськи
+
+
+
+Сейчас в душ схожу постриглась быстренько, и к вам
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:01
+
+
+
+Rustam Khalimov
+
+
+
+Алсу приедет?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:01
+
+
+
+Рустам Брат Леськи
+
+
+
+Алсу тоже будет, ей тоже ещё время надо. Ужин готовит
+
+
+
+
+
+
+
+👌
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:02
+
+
+
+Seagate 4 ТБ Внутренний жесткий диск (ST4000VN006)
https://ozon.ru/t/d7FtTmx
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:02
+
+
+
+Вот этот думаю брать уж
+
+
+
+
+
+
+
+
+
+
+
+13:03
+
+
+
+Я в Яндекс маркете сплит брал просто. Там рассрочка, удобно
+
+
+
+
+
+
+
+
+
+
+
+13:03
+
+
+
+В озоне нет такого, поэтому ещё думаю
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:14
+
+
+
+Rustam Khalimov
+
+
+
+Можешь купаты купить, приготовим, если желание есть
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:29
+
+
+
+Рустам Брат Леськи
+
+
+
+Окей, купим
+
+
+
+
+
+
+
+👌
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:17
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+Соболев LIVE 30.03.2026 17:19:01
+
+
+
+
+
+Вайб 2026. Кто ещё может смотреть видео - тот молодец.
@sobolevtam
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:18
+
+
+
+Рустам Брат Леськи
+
+
+
+Привет. С днюхой тебя! Желаю крепкого здоровья и высоких зарплат!
+
+
+
+
+
+
+
+🤝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+00:37
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Спасибо!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:50
+
+
+
+Привет. Вчера забрал
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:51
+
+
+
+Но что-то немного со звуком работают, один который до этого стоял почти не шумел, подумываю что опять попал на брак
+
+
+
+
+
+
+
+
+
+
+
+13:51
+
+
+
+Но оба в заводской упаковке были
+
+
+
+
+
+
+
+
+
+
+
+13:52
+
+
+
+Пока сильно не разбирался, вечером по одному попробую посмотрю
+
+
+
+
+
+
+
+
+
+
+
+13:54
+
+
+
+Ну может конечно это норм. Шумят когда я нагружаю вроде
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:54
+
+
+
+Rustam Khalimov
+
+
+
+Под нагрузкой должны шуметь но не сильно
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:14
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:15
+
+
+
+Ну вот такая скорость. Подключен через локалку, никак уж там 100мб/с даже не достигаются, максимум около 20 только
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:16
+
+
+
+Rustam Khalimov
+
+
+
+Что то не то
+
+
+
+
+
+
+
+
+
+
+
+17:31
+
+
+
+А порты роутера у тебя какие?
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:34
+
+
+
+Вот у меня весь гигабит выдает
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:44
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+100мб
+
+
+
+
+
+
+
+
+
+
+
+17:44
+
+
+
+Но тут же даже не близко
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:44
+
+
+
+Rustam Khalimov
+
+
+
+
+
+100мегабит это 11 мегабайт в сек
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:44
+
+
+
+Рустам Брат Леськи
+
+
+
+Может с телефона потомучто по вайфаю
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:45
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Может быть или прослойки какие то
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:45
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Хм🤔
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:47
+
+
+
+Но походу норм это все
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:48
+
+
+
+Rustam Khalimov
+
+
+
+У меня с телефона так же, в сеть упирается, 105-110 мегабайт в сек
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:48
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+17:48
+
+
+
+Rustam Khalimov
+
+
+
+20-60 это мало
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+Пока такой прогресс
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+Рустам Брат Леськи
+
+
+
+Ничего себе. Круто
+
+
+
+
+
+
+
+
+
+
+
+12:16
+
+
+
+У меня +10 к этому
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:30
+
+
+
+Rustam Khalimov
+
+
+
+Ну тоже скинешь со временем
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:54
+
+
+
+Rustam Khalimov
+
+
+
+Как там твой NAS?
+
+
+
+
+
+
+
+
+
+
+
+
+
+18:56
+
+
+
+Рустам Брат Леськи
+
+
+
+Все норм. Сегодня в шкаф наконец разместил
+
+
+
+
+
+
+
+
+
+
+
+18:58
+
+
+
+В первый день как включил индексирование, больше суток без остановки шумел вентилятор. По программе температура процессора больше 80 градусов стояла.
+
+
+
+
+
+
+
+
+
+
+
+18:59
+
+
+
+Сейчас уже норм. Тихонечко стоит просто
+
+
+
+
+
+
+
+
+
+
+
+19:00
+
+
+
+И жесткие тоже норм уже. Они только при первом включении потрескиванием просто как будто включаются, а дальше все норм работают
+
+
+
+
+
+
+
+
+
+
+
+19:01
+
+
+
+Но вентилятор если включается то шумно
+
+
+
+
+
+
+
+
+
+
+
+19:01
+
+
+
+Я уже по форумам все поизучал как быть. Решают шум вентилятора заменой на другой
+
+
+
+
+
+
+
+
+
+
+
+19:02
+
+
+
+Но в итоге уже нормально
+
+
+
+
+
+
+
+
+
+
+
+19:02
+
+
+
+Только английский язык конечно минус
+
+
+
+
+
+
+
+
+
+
+
+
+
+19:38
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:04
+
+
+
+Rustam Khalimov
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:05
+
+
+
+Проксмокс надо ставить, но ssd надо
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:14
+
+
+
+Рустам Брат Леськи
+
+
+
+Я заказал с алика
+
+
+
+
+
+
+
+
+
+
+
+20:14
+
+
+
+Через неделю придет примерно
+
+
+
+
+
+
+
+
+
+
+
+20:14
+
+
+
+500гб
+
+
+
+
+
+
+
+
+
+
+
+20:15
+
+
+
+Проксмос сильно загрузит наверно.
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:52
+
+
+
+Rustam Khalimov
+
+
+
+Да, нормально жрет он, оперативки мало будет 8гб
+
+
+
+
+
+
+
+
+
+
+
+20:54
+
+
+
+Хотя я свой смотрю, у меня 2ГБ он максимум ест
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:54
+
+
+
+Рустам Брат Леськи
+
+
+
+Я задумывался может на синолоджи перейти просто
+
+
+
+
+
+
+
+
+
+
+
+20:54
+
+
+
+Купить синолоджи, а этот просто выставить на продажу
+
+
+
+
+
+
+
+
+
+
+
+20:54
+
+
+
+Но и этот норм, пока так уж
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:55
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Ты характеристики синолоджи видел и цены?
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:56
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+Цены просто смотрел, примерно до 30 вроде такие же на 2
+
+
+
+
+
+
+
+
+
+
+
+20:56
+
+
+
+А характеристики не смотрел
+
+
+
+
+
+
+
+
+
+
+
+20:57
+
+
+
+Да нет уж, это просто задумывался
+
+
+
+
+
+
+
+
+
+
+
+20:57
+
+
+
+Но вот только на 4 хдд надо было это точно
+
+
+
+
+
+
+
+
+
+
+
+
+
+20:57
+
+
+
+Rustam Khalimov
+
+
+
+
+
+Это просто операционка и всё, ты за нее переплатишь, а железо там раз в 7 слабее
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:03
+
+
+
+Если тебе самому ковыряться лень или нет времени, я могу тебе поставить на него Xpenology или проксмокс
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:17
+
+
+
+Рустам Брат Леськи
+
+
+
+Вижу да разница оказывается огромная
+
+
+
+
+
+
+
+
+
+
+
+21:18
+
+
+
+
+
+Не, пока на угрине чисто буду
+
+
+
+
+
+
+
+
+
+
+
+21:18
+
+
+
+Может думаю со временем и обновление с русским языком придёт
+
+
+
+
+
+
+
+
+
+
+
+21:19
+
+
+
+Так то я все уже в общем там разобрался
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:22
+
+
+
+Rustam Khalimov
+
+
+
+Хорошо, просто всё намного удобнее может быть, если разобраться и в докере что то развернуть
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:23
+
+
+
+Рустам Брат Леськи
+
+
+
+Да в докере интересно конечно что нибудь сделать
+
+
+
+
+
+
+
+
+
+
+
+21:23
+
+
+
+Сегодня немного почитал про него тоже
+
+
+
+
+
+
+
+
+
+
+
+
+
+21:23
+
+
+
+Rustam Khalimov
+
+
+
+Проблема, что он у тебя какой то урезанный
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+12:59
+
+
+
+Рустам Брат Леськи
+
+
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:03
+
+
+
+Rustam Khalimov
+
+
+
+Для камер или сервера?
+
+
+
+
+
+
+
+
+
+
+
+
+
+13:21
+
+
+
+Рустам Брат Леськи
+
+
+
+Для камер
+
+
+
+
+
+
+
+
+
+
+
+13:21
+
+
+
+На объекте поменял
+
+
+
+
+
+
+
+🔥
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg
new file mode 100644
index 0000000..7fdd086
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg
new file mode 100644
index 0000000..78caf22
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg
new file mode 100644
index 0000000..04de83b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg
new file mode 100644
index 0000000..5558428
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg
new file mode 100644
index 0000000..cc09f2d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg
new file mode 100644
index 0000000..64929f2
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg
new file mode 100644
index 0000000..a1ab3a2
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg
new file mode 100644
index 0000000..bebb101
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg
new file mode 100644
index 0000000..b197aac
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg
new file mode 100644
index 0000000..236aadd
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg
new file mode 100644
index 0000000..87fbc37
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg
new file mode 100644
index 0000000..23fd391
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg
new file mode 100644
index 0000000..ada789d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg
new file mode 100644
index 0000000..4727fef
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg
new file mode 100644
index 0000000..48c971d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg
new file mode 100644
index 0000000..9b71112
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg
new file mode 100644
index 0000000..0b31291
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg
new file mode 100644
index 0000000..11baf12
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg
new file mode 100644
index 0000000..76cdbf0
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg
new file mode 100644
index 0000000..64a916c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg
new file mode 100644
index 0000000..d4ffb16
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg
new file mode 100644
index 0000000..6ff626c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg
new file mode 100644
index 0000000..9d27bc4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg
new file mode 100644
index 0000000..4356cb9
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg
new file mode 100644
index 0000000..0f8c9f3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg
new file mode 100644
index 0000000..e6ecab3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg
new file mode 100644
index 0000000..e6be01f
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg
new file mode 100644
index 0000000..56632b6
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg
new file mode 100644
index 0000000..d41ffd0
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg
new file mode 100644
index 0000000..17858e8
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg
new file mode 100644
index 0000000..8c4e066
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg
new file mode 100644
index 0000000..0de0baa
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg
new file mode 100644
index 0000000..be39192
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg
new file mode 100644
index 0000000..4a05fd3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg
new file mode 100644
index 0000000..be39192
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg
new file mode 100644
index 0000000..4a05fd3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg
new file mode 100644
index 0000000..992cdf0
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg
new file mode 100644
index 0000000..99dd94e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg
new file mode 100644
index 0000000..5c6ae01
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg
new file mode 100644
index 0000000..f612b5d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg
new file mode 100644
index 0000000..3298fa4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg
new file mode 100644
index 0000000..a349150
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg
new file mode 100644
index 0000000..b023f8f
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg
new file mode 100644
index 0000000..59eec84
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg
new file mode 100644
index 0000000..7f6f1ad
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg
new file mode 100644
index 0000000..f4126fb
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg
new file mode 100644
index 0000000..afd870c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg
new file mode 100644
index 0000000..0a374c8
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg
new file mode 100644
index 0000000..3297d31
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg
new file mode 100644
index 0000000..5f1d771
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg
new file mode 100644
index 0000000..7258ca3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg
new file mode 100644
index 0000000..a20087d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg
new file mode 100644
index 0000000..a715ccf
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg
new file mode 100644
index 0000000..94ee828
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg
new file mode 100644
index 0000000..f36603a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg
new file mode 100644
index 0000000..9fd4df6
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg
new file mode 100644
index 0000000..c2f6027
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg
new file mode 100644
index 0000000..8ebc1f5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg
new file mode 100644
index 0000000..720a17d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg
new file mode 100644
index 0000000..a7169ad
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg
new file mode 100644
index 0000000..1063bda
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg
new file mode 100644
index 0000000..cba1687
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg
new file mode 100644
index 0000000..41426e4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg
new file mode 100644
index 0000000..4fefd55
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg
new file mode 100644
index 0000000..e5753c7
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg
new file mode 100644
index 0000000..0f83909
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg
new file mode 100644
index 0000000..0ff24d3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg
new file mode 100644
index 0000000..9374a53
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg
new file mode 100644
index 0000000..a661e46
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg
new file mode 100644
index 0000000..5b06532
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg
new file mode 100644
index 0000000..efc3f0c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg
new file mode 100644
index 0000000..f3dd432
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg
new file mode 100644
index 0000000..c20a791
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg
new file mode 100644
index 0000000..45c03b3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg
new file mode 100644
index 0000000..3fa03a2
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg
new file mode 100644
index 0000000..48e2388
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg
new file mode 100644
index 0000000..282df6e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg
new file mode 100644
index 0000000..507f907
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg
new file mode 100644
index 0000000..434fe75
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg
new file mode 100644
index 0000000..9469d0e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg
new file mode 100644
index 0000000..ee7876a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg
new file mode 100644
index 0000000..beb8bd5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg
new file mode 100644
index 0000000..a5d27d8
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg
new file mode 100644
index 0000000..3f76e2e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg
new file mode 100644
index 0000000..9d71c4b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg
new file mode 100644
index 0000000..9522a69
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg
new file mode 100644
index 0000000..7e08807
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg
new file mode 100644
index 0000000..e2f7231
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg
new file mode 100644
index 0000000..61770df
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg
new file mode 100644
index 0000000..54a2b15
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg
new file mode 100644
index 0000000..abe9e8c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg
new file mode 100644
index 0000000..6491702
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg
new file mode 100644
index 0000000..c965440
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg
new file mode 100644
index 0000000..d2dd8c8
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg
new file mode 100644
index 0000000..560e3c2
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg
new file mode 100644
index 0000000..78a6235
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg
new file mode 100644
index 0000000..aba5bfc
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg
new file mode 100644
index 0000000..046976e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg
new file mode 100644
index 0000000..767a262
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg
new file mode 100644
index 0000000..098cc78
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg
new file mode 100644
index 0000000..91610fc
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg
new file mode 100644
index 0000000..8c249d5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg
new file mode 100644
index 0000000..b9a23c4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg
new file mode 100644
index 0000000..c3ab513
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg
new file mode 100644
index 0000000..08351e4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg
new file mode 100644
index 0000000..4087bd4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg
new file mode 100644
index 0000000..c49b7d5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg
new file mode 100644
index 0000000..b9e5b04
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg
new file mode 100644
index 0000000..60c2645
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg
new file mode 100644
index 0000000..640285b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg
new file mode 100644
index 0000000..ed94ec3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg
new file mode 100644
index 0000000..82d87e4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg
new file mode 100644
index 0000000..dfdec8b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg
new file mode 100644
index 0000000..44d3c3c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg
new file mode 100644
index 0000000..3fad496
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg
new file mode 100644
index 0000000..ad2059a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg
new file mode 100644
index 0000000..6c3d951
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg
new file mode 100644
index 0000000..11f96b5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg
new file mode 100644
index 0000000..1787c38
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg
new file mode 100644
index 0000000..13c292b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg
new file mode 100644
index 0000000..6e77004
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg
new file mode 100644
index 0000000..2ff401a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg
new file mode 100644
index 0000000..f81ca93
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg
new file mode 100644
index 0000000..f5ef0f8
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg
new file mode 100644
index 0000000..a7a3d47
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg
new file mode 100644
index 0000000..7f144d6
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg
new file mode 100644
index 0000000..da8509e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg
new file mode 100644
index 0000000..1f07dce
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg
new file mode 100644
index 0000000..f668a8f
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg
new file mode 100644
index 0000000..407898c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg
new file mode 100644
index 0000000..a2d83e0
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg
new file mode 100644
index 0000000..5b19871
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg
new file mode 100644
index 0000000..cf20f9c
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg
new file mode 100644
index 0000000..5decfa7
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg
new file mode 100644
index 0000000..007e9d3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg
new file mode 100644
index 0000000..c609b06
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg
new file mode 100644
index 0000000..b91955a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg
new file mode 100644
index 0000000..83ec719
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg
new file mode 100644
index 0000000..2abffbe
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg
new file mode 100644
index 0000000..9287a57
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg
new file mode 100644
index 0000000..1ad5903
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg
new file mode 100644
index 0000000..dc89b9e
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg
new file mode 100644
index 0000000..f3bf9b1
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg
new file mode 100644
index 0000000..2c0626d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg
new file mode 100644
index 0000000..8657e92
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg
new file mode 100644
index 0000000..6f316a5
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg
new file mode 100644
index 0000000..ffc512a
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg
new file mode 100644
index 0000000..48b47d3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..7907343
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..03d9786
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..dace5fa
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..0397a01
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..84e94bd
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..eb86a25
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..90642ff
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..3034a96
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..dc4af9b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..5afbc7b
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..2dec6ed
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..d1df3cd
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..076a354
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..563feab
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..3163ef3
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..002a155
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg
new file mode 100644
index 0000000..a96a1e4
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg differ
diff --git a/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg
new file mode 100644
index 0000000..575f83d
Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs b/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs
new file mode 100644
index 0000000..0aea9df
Binary files /dev/null and b/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs differ
diff --git a/ChatExport_2026-04-06/video_files/185.mp4 b/ChatExport_2026-04-06/video_files/185.mp4
new file mode 100644
index 0000000..c808ae0
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/185.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4 b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4
new file mode 100644
index 0000000..24a55e0
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg
new file mode 100644
index 0000000..383d31d
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/IMG_9101.MP4 b/ChatExport_2026-04-06/video_files/IMG_9101.MP4
new file mode 100644
index 0000000..4350b42
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9101.MP4 differ
diff --git a/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg b/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg
new file mode 100644
index 0000000..6468967
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/IMG_9327.MP4 b/ChatExport_2026-04-06/video_files/IMG_9327.MP4
new file mode 100644
index 0000000..041681f
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9327.MP4 differ
diff --git a/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg b/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg
new file mode 100644
index 0000000..dee5a73
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/animation.gif.mp4 b/ChatExport_2026-04-06/video_files/animation.gif.mp4
new file mode 100644
index 0000000..feca56a
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/animation.gif.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg
new file mode 100644
index 0000000..d13fca7
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4 b/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4
new file mode 100644
index 0000000..7150bf9
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4 b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4
new file mode 100644
index 0000000..c4fdd45
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg
new file mode 100644
index 0000000..593023c
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4 b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4
new file mode 100644
index 0000000..4cb1e5c
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg
new file mode 100644
index 0000000..3deec6e
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4 b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4
new file mode 100644
index 0000000..28e1172
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4 differ
diff --git a/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg
new file mode 100644
index 0000000..0e6ef47
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg differ
diff --git a/ChatExport_2026-04-06/video_files/этопечально-печально.mp4 b/ChatExport_2026-04-06/video_files/этопечально-печально.mp4
new file mode 100644
index 0000000..3ba5194
Binary files /dev/null and b/ChatExport_2026-04-06/video_files/этопечально-печально.mp4 differ
diff --git a/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg b/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg
new file mode 100644
index 0000000..0632f2f
Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg differ
diff --git a/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg b/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg
new file mode 100644
index 0000000..ad3170e
Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg differ
diff --git a/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg b/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg
new file mode 100644
index 0000000..64d9e5a
Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg differ
diff --git a/backend/src/Contracts/Conversations/Domain/Chat.cs b/backend/src/Contracts/Conversations/Domain/Chat.cs
index 6d2b79a..58082b6 100644
--- a/backend/src/Contracts/Conversations/Domain/Chat.cs
+++ b/backend/src/Contracts/Conversations/Domain/Chat.cs
@@ -36,17 +36,21 @@ public sealed class Chat : AggregateRoot
public string? Avatar { get; private set; }
public DateTime CreatedAt { get; private set; }
public long LastMessageSequenceId { get; private set; }
+ public bool IsImporting { get; private set; }
+ public Guid? ImportJobId { get; private set; }
private readonly List _members = new();
public IReadOnlyCollection Members => _members.AsReadOnly();
- private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
+ private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null, bool isImporting = false, Guid? importJobId = null) : base(id)
{
Type = type;
Name = name;
Avatar = avatar;
Description = description;
CreatedAt = DateTime.UtcNow;
+ IsImporting = isImporting;
+ ImportJobId = importJobId;
}
public static Chat CreatePersonal()
@@ -63,13 +67,18 @@ public sealed class Chat : AggregateRoot
return chat;
}
- public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
+ public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null, bool isImporting = false, Guid? importJobId = null)
{
- var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
+ var chat = new Chat(Guid.NewGuid(), type, name, avatar, description, isImporting, importJobId);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
+ public void CompleteImport()
+ {
+ IsImporting = false;
+ }
+
public void AddMember(Guid userId, string role = "member")
{
if (_members.Any(m => m.UserId == userId))
diff --git a/backend/src/Contracts/Conversations/Domain/ChatConstants.cs b/backend/src/Contracts/Conversations/Domain/ChatConstants.cs
new file mode 100644
index 0000000..27a7a7c
--- /dev/null
+++ b/backend/src/Contracts/Conversations/Domain/ChatConstants.cs
@@ -0,0 +1,8 @@
+namespace Knot.Contracts.Conversations.Domain;
+
+public static class ChatConstants
+{
+ public const int DefaultMessageQueryLimit = 50;
+ public const int MaxSharedMediaQueryLimit = 1000;
+ public const int MaxGroupNameLength = 100;
+}
diff --git a/backend/src/Contracts/Conversations/Domain/ChatErrors.cs b/backend/src/Contracts/Conversations/Domain/ChatErrors.cs
new file mode 100644
index 0000000..cca14d2
--- /dev/null
+++ b/backend/src/Contracts/Conversations/Domain/ChatErrors.cs
@@ -0,0 +1,19 @@
+using Knot.Shared.Kernel;
+
+namespace Knot.Contracts.Conversations.Domain;
+
+public static class ChatErrors
+{
+ public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Чат не найден");
+ public static readonly Error OnlyOwnerCanUpdate = new Error("Chat.OnlyOwnerCanUpdate", "Только владелец может редактировать чат");
+ public static readonly Error Unauthorized = new Error("Chat.Unauthorized", "Нет доступа к этому чату");
+ public static readonly Error FoldersDisabled = new Error("Chat.FoldersDisabled", "Папки отключены");
+ public static readonly Error FileEmpty = new Error("Chat.FileEmpty", "Файл пуст");
+ public static Error FileTooLarge(long maxMb) => new Error("Chat.FileTooLarge", $"Файл слишком большой (максимум {maxMb} МБ)");
+ public static readonly Error ChatsNotFound = new Error("Chat.NotFound", "Чат не найден");
+ public static readonly Error ChatsForbidden = new Error("Chat.Forbidden", "Доступ запрещен");
+ public static readonly Error MediaDisabled = new Error("Chat.MediaDisabled", "Медиафайлы отключены");
+ public static readonly Error PollsDisabled = new Error("Chat.PollsDisabled", "Опросы отключены");
+ public static readonly Error NotFound = new Error("Chat.NotFound", "Не найдено");
+ public static readonly Error NotMember = new Error("Chat.NotMember", "Вы не являетесь участником чата");
+}
diff --git a/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs
index 8feac1a..22013c6 100644
--- a/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs
+++ b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs
@@ -10,7 +10,7 @@ public interface IMessageRepository
Task GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken);
Task> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
- Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken);
+ Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken);
Task GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
Task UpdateAsync(Message message, CancellationToken cancellationToken);
diff --git a/backend/src/Host/Program.cs b/backend/src/Host/Program.cs
index f59c0ea..e73f20a 100644
--- a/backend/src/Host/Program.cs
+++ b/backend/src/Host/Program.cs
@@ -41,6 +41,7 @@ using MediatR;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
+Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var builder = WebApplication.CreateBuilder(args);
@@ -81,6 +82,7 @@ builder.Services.AddStoriesModule(builder.Configuration);
builder.Services.AddKlipyModule();
builder.Services.AddAdminModule();
builder.Services.AddWebRtcModule();
+builder.Services.AddTelegramImportModule();
builder.Services.AddSharedInfrastructure(builder.Configuration);
// CQRS / MediatR для команд в Host (например, AdminController)
@@ -93,7 +95,10 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
typeof(Knot.Modules.Stories.DependencyInjection).Assembly,
typeof(Knot.Modules.Klipy.DependencyInjection).Assembly,
typeof(Knot.Modules.Relations.DependencyInjection).Assembly,
- typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly
+ typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly,
+ typeof(Knot.Modules.TelegramImport.DependencyInjection).Assembly,
+ typeof(Knot.Modules.Auth.Infrastructure.Persistence.AuthDbContext).Assembly,
+ typeof(Knot.Modules.Profiles.DependencyInjection).Assembly
));
// Настройка CORS
diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs
index 01f1db6..406f0ed 100644
--- a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs
+++ b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs
@@ -8,7 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
using Knot.Contracts.Messaging.Application.Abstractions;
-using Knot.Contracts.Storage.Abstractions;
+using Knot.Shared.Kernel.Storage;
using Knot.Contracts.Stories.Infrastructure.Persistence;
using Knot.Modules.Admin.Application.Admin.DTOs;
using MediatR;
@@ -51,7 +51,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler !orphanMessages.Any(om => om.Id == m.Id))
+ .Where(m => !orphanMessages.Any(om => om.Id == m.Id) && !m.IsDeleted)
.ToList();
var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList();
diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs
index e0cc42c..1ba60d5 100644
--- a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs
+++ b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -8,6 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
using Knot.Contracts.Messaging.Application.Abstractions;
+using Knot.Shared.Kernel.Storage;
using Knot.Contracts.Stories.Infrastructure.Persistence;
using Knot.Modules.Admin.Application.Admin.DTOs;
using MediatR;
@@ -23,17 +24,20 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler> Handle(CleanDryRunQuery request, CancellationToken ct)
@@ -46,25 +50,42 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler m.MediaUrl != null);
var orphanedMessageCount = orphanedMessages.Count;
- var validIds = new HashSet();
- foreach (var msg in orphanedMessages.Where(m => m.MediaUrl != null))
- {
- var parts = msg.MediaUrl.Split('/');
- var fileId = parts.LastOrDefault();
- if (!string.IsNullOrEmpty(fileId))
- {
- validIds.Add(fileId);
- }
- }
-
+ var allMessages = await _messageService.GetAllMessagesAsync(ct);
+ var keptMessages = allMessages
+ .Where(m => !orphanedMessages.Any(om => om.Id == m.Id) && !m.IsDeleted)
+ .ToList();
+ var allUsers = await _authDbContext.Users.ToListAsync(ct);
var stories = await _storyCollection.GetAllAsync(ct);
+
+ var validUrls = new HashSet();
+
+ var activeMessageUrls = keptMessages.Where(m => m.Media != null).SelectMany(m => m.Media!).Select(x => x.Url).Where(u => !string.IsNullOrEmpty(u));
+ var activeChatUrls = _chatsDbContext.Chats.Select(c => c.Avatar).Where(u => !string.IsNullOrEmpty(u));
+ var activeUserUrls = allUsers.Select(u => u.Avatar).Where(u => !string.IsNullOrEmpty(u));
+ var activeStoryUrls = stories.Select(s => s.MediaUrl).Where(u => !string.IsNullOrEmpty(u));
+
+ foreach (var u in activeMessageUrls) validUrls.Add(u!);
+ foreach (var u in activeChatUrls) validUrls.Add(u!);
+ foreach (var u in activeUserUrls) validUrls.Add(u!);
+ foreach (var u in activeStoryUrls) validUrls.Add(u!);
+
+ var validFileIds = validUrls
+ .Where(u => u.Contains("/api/files/"))
+ .Select(u => u.Split('/').Last())
+ .ToHashSet();
+
+ var allMinioFiles = await _fileStorage.ListFilesAsync();
+ long orphanedFileSize = allMinioFiles
+ .Where(f => !validFileIds.Contains(f.FileId))
+ .Sum(f => f.Size);
+
var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow);
var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0);
return Result.Success(new CleanDryRunResult(
orphanedMessageCount,
orphanedMediaCount,
- 0,
+ orphanedFileSize,
expiredStoriesCount,
expiredStoriesSize
));
diff --git a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs
index 18c6eb0..c49e6c8 100644
--- a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs
+++ b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs
@@ -131,6 +131,17 @@ public static class AdminEndpoints
return Results.Ok(result.Value);
});
+ group.MapPost("clean/run", async (ISender sender, CancellationToken ct) =>
+ {
+ var result = await sender.Send(new CleanRunCommand(), ct);
+ if (!result.IsSuccess)
+ {
+ Console.WriteLine($"[Admin] Cleanup Run Error: {result.Error.Description}");
+ return Results.BadRequest(new { error = result.Error.Description });
+ }
+ return Results.Ok(result.Value);
+ });
+
group.MapGet("timezones", () =>
{
// Получаем все системные часовые пояса и формируем удобный для фронтенда формат
diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs b/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs
deleted file mode 100644
index 6c9bdb2..0000000
--- a/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Knot.Shared.Kernel;
-
-namespace Knot.Modules.Conversations.Application.Abstractions;
-
-///
-/// Unit of Work специфичный для модуля Chats.
-///
-public interface IChatsUnitOfWork : IUnitOfWork
-{
-}
-
diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs b/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs
deleted file mode 100644
index 5565e80..0000000
--- a/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Knot.Shared.Kernel;
-
-namespace Knot.Modules.Conversations.Application.Abstractions;
-
-public interface IUserDeleterService
-{
- Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
-}
diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs b/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs
deleted file mode 100644
index d4a8761..0000000
--- a/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System;
-
-namespace Knot.Modules.Conversations.Application.Abstractions;
-
-public interface IUserStatusService
-{
- bool IsUserOnline(string userId);
-}
diff --git a/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs
index 64189be..68cb10f 100644
--- a/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs
+++ b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs
@@ -4,8 +4,8 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Storage;
-using Knot.Modules.Conversations.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
using SixLabors.ImageSharp;
diff --git a/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs b/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs
index 3bd1d4a..462cd6a 100644
--- a/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs
+++ b/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs
@@ -2,8 +2,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Shared.Kernel;
-using Knot.Modules.Conversations.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
using MediatR;
using System.Linq;
diff --git a/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs b/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs
index a2e327f..fb8e8cd 100644
--- a/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs
+++ b/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs
@@ -1,7 +1,7 @@
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations.Application.Chats.Create;
diff --git a/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs b/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs
index e9445ab..7251d31 100644
--- a/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs
+++ b/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs b/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs
index 0d0f8d1..951cac2 100644
--- a/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs
+++ b/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -147,7 +147,9 @@ internal sealed class GetChatsQueryHandler : IQueryHandler
{
private readonly IChatRepository _chatRepository;
+ private readonly IMessageRepository _messageRepository;
+ private readonly IFileStorageService _fileStorage;
private readonly IChatsUnitOfWork _uow;
- public LeaveOrDeleteChatCommandHandler(IChatRepository chatRepository, IChatsUnitOfWork uow)
+ public LeaveOrDeleteChatCommandHandler(
+ IChatRepository chatRepository,
+ IMessageRepository messageRepository,
+ IFileStorageService fileStorage,
+ IChatsUnitOfWork uow)
{
_chatRepository = chatRepository;
+ _messageRepository = messageRepository;
+ _fileStorage = fileStorage;
_uow = uow;
}
@@ -36,13 +45,18 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler(ChatErrors.Unauthorized);
}
- if (chat.Type == ChatType.Group)
+ // If it's a private chat or the last member leaving a group, delete everything
+ bool shouldDeleteEverything = chat.Type != ChatType.Group || chat.Members.Count <= 1;
+
+ if (chat.Type == ChatType.Group && !shouldDeleteEverything)
{
chat.RemoveMember(request.UserId);
_chatRepository.Update(chat);
}
else
{
+ // DELETE ALL MESSAGES AND FILES FIRST
+ await DeleteChatMediaAndMessagesAsync(chat.Id, cancellationToken);
_chatRepository.Remove(chat);
}
@@ -50,5 +64,45 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler Members,
List Messages,
- int UnreadCount
+ int UnreadCount,
+ bool IsImporting = false,
+ Guid? ImportJobId = null
);
diff --git a/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs
index 70a9024..b7cbe69 100644
--- a/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs
+++ b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs
@@ -1,4 +1,4 @@
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using System;
namespace Knot.Modules.Conversations.Application.DTOs;
diff --git a/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs
index 638431b..349145c 100644
--- a/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs
+++ b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
namespace Knot.Modules.Conversations.Application.DTOs;
diff --git a/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs b/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs
index 1be4fec..beb6b97 100644
--- a/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs
@@ -1,5 +1,5 @@
-using Knot.Modules.Conversations.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs b/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs
index d136ffb..bf3bf93 100644
--- a/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs
@@ -1,5 +1,5 @@
-using Knot.Modules.Conversations.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs
index 9b2d667..d90fce4 100644
--- a/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs
@@ -1,5 +1,5 @@
-using global::Knot.Modules.Conversations.Application.Abstractions;
-using global::Knot.Modules.Conversations.Domain;
+using global::Knot.Contracts.Conversations.Application.Abstractions;
+using global::Knot.Contracts.Conversations.Domain;
using global::Knot.Modules.Conversations.Infrastructure.SignalR;
using global::Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs b/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs
index c90ab27..a590b22 100644
--- a/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs
@@ -5,9 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
@@ -39,12 +39,21 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler();
var userIdsToFetch = new HashSet();
@@ -88,7 +97,8 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler
{
var mType = media.Type?.ToLower() ?? "file";
- var isGif = mType == "image" && media.Url != null && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
+ var filename = media.Filename?.ToLower() ?? "";
+ var url = media.Url?.ToLower() ?? "";
+
+ var isGif = mType == "gif" ||
+ (mType == "image" && (filename.EndsWith(".mp4") || filename.EndsWith(".gif") || url.EndsWith(".gif") || filename.Contains("gif"))) ||
+ (mType == "video" && (filename.Contains("animation") || filename.Contains("gif")));
if (filterType == "gifs")
{
diff --git a/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs b/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs
index f2652bc..d6641b0 100644
--- a/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs
@@ -1,7 +1,7 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs b/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs
index 445d794..51a2222 100644
--- a/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs
@@ -1,7 +1,7 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs b/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs
index d0f9c3b..6a72426 100644
--- a/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs
@@ -1,7 +1,7 @@
using MediatR;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
namespace Knot.Modules.Conversations.Application.Messages.Read;
diff --git a/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs b/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs
index 69250e8..1bd2c5f 100644
--- a/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.Conversations.Application.DTOs;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
diff --git a/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs b/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs
index 35ed35e..5accf6d 100644
--- a/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs
@@ -1,8 +1,8 @@
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Contracts.Settings.Application.Abstractions;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
namespace Knot.Modules.Conversations.Application.Messages.Send;
diff --git a/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs b/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs
index 236ca92..8ba2e96 100644
--- a/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs
@@ -1,4 +1,4 @@
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using System;
using System.IO;
using System.Threading;
diff --git a/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs b/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs
index a78d58c..e24aa91 100644
--- a/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs
+++ b/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs
@@ -2,8 +2,8 @@ using System.Text.RegularExpressions;
using Knot.Contracts.Auth.Application.Abstractions;
using Knot.Contracts.Auth.Domain;
using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Storage;
using MediatR;
@@ -55,15 +55,18 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler m.Id != msg.Id &&
- m is MediaMessage mm && mm.Media.Any(ame => ame.Url == media.Url));
-
- if (!isUsedElsewhere)
+ if (!string.IsNullOrEmpty(media.Url))
{
- var fileId = ExtractFileId(media.Url);
- if (!string.IsNullOrEmpty(fileId))
+ bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id &&
+ m is MediaMessage mm && mm.Media.Any(ame => !string.IsNullOrEmpty(ame.Url) && ame.Url == media.Url));
+
+ if (!isUsedElsewhere)
{
- await _fileStorage.DeleteFileAsync(fileId);
+ var fileId = ExtractFileId(media.Url);
+ if (!string.IsNullOrEmpty(fileId))
+ {
+ await _fileStorage.DeleteFileAsync(fileId);
+ }
}
}
}
diff --git a/backend/src/Modules/Conversations/DependencyInjection.cs b/backend/src/Modules/Conversations/DependencyInjection.cs
index 9f23bd0..9e60630 100644
--- a/backend/src/Modules/Conversations/DependencyInjection.cs
+++ b/backend/src/Modules/Conversations/DependencyInjection.cs
@@ -1,16 +1,14 @@
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
-using Knot.Contracts.Messaging.Application.Abstractions;
-using Knot.Contracts.Messaging.Domain;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
+using Knot.Modules.Conversations.Infrastructure.Services;
using Knot.Shared.Kernel;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using ConversationsAbstractions = Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Messaging.Application.Abstractions;
namespace Knot.Modules.Conversations;
@@ -29,21 +27,22 @@ public static class DependencyInjection
var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017";
- services.AddScoped(sp => sp.GetRequiredService());
+ services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped();
services.AddScoped();
services.AddScoped();
- services.AddScoped();
+ services.AddScoped();
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
- services.AddScoped();
+ services.AddScoped();
services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
return services;
}
}
diff --git a/backend/src/Modules/Conversations/Domain/Chat.cs b/backend/src/Modules/Conversations/Domain/Chat.cs
deleted file mode 100644
index 73417e9..0000000
--- a/backend/src/Modules/Conversations/Domain/Chat.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-using Knot.Shared.Kernel;
-
-namespace Knot.Modules.Conversations.Domain;
-
-public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent;
-public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent;
-
-///
-/// Тип чата: личный или групповой.
-///
-public enum ChatType
-{
- Personal,
- Group,
- Favorites
-}
-
-///
-/// Роль участника в чате.
-///
-public static class ChatRole
-{
- public const string Owner = "owner";
- public const string Admin = "admin";
- public const string Member = "member";
-}
-
-///
-/// Сущность чата (Агрегат).
-///
-public sealed class Chat : AggregateRoot
-{
- public ChatType Type { get; private set; }
- public string? Name { get; private set; }
- public string? Description { get; private set; }
- public string? Avatar { get; private set; }
- public DateTime CreatedAt { get; private set; }
- public long LastMessageSequenceId { get; private set; }
-
- private readonly List _members = new();
- public IReadOnlyCollection Members => _members.AsReadOnly();
-
- private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id)
- {
- Type = type;
- Name = name;
- Avatar = avatar;
- Description = description;
- CreatedAt = DateTime.UtcNow;
- }
-
- ///
- /// Создает личный чат между двумя пользователями.
- ///
- public static Chat CreatePersonal()
- {
- var chat = new Chat(Guid.NewGuid(), ChatType.Personal, null, null);
- chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
- return chat;
- }
-
- ///
- /// Создает групповой чат.
- ///
- public static Chat CreateGroup(string name, string? avatar = null)
- {
- var chat = new Chat(Guid.NewGuid(), ChatType.Group, name, avatar);
- chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
- return chat;
- }
-
- ///
- /// Фабричный метод для создания чата.
- ///
- public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null)
- {
- var chat = new Chat(Guid.NewGuid(), type, name, avatar, description);
- chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
- return chat;
- }
-
- public void AddMember(Guid userId, string role = "member")
- {
- if (_members.Any(m => m.UserId == userId))
- {
- return;
- }
-
- _members.Add(new ChatMember(Id, userId, role));
- RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId));
- }
-
- public void RemoveMember(Guid userId)
- {
- var member = _members.FirstOrDefault(m => m.UserId == userId);
- if (member != null)
- {
- _members.Remove(member);
- }
- }
-
- public void UpdateName(string name) => Name = name;
-
- public void UpdateDescription(string? description) => Description = description;
-
- public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl;
-
- public long IncrementSequenceId()
- {
- return ++LastMessageSequenceId;
- }
-}
-
-///
-/// Участник чата.
-///
-public sealed class ChatMember : Entity
-{
- public Guid ChatId { get; private set; }
- public Guid UserId { get; private set; }
- public string Role { get; private set; }
- public DateTime JoinedAt { get; private set; }
- public bool IsPinned { get; private set; }
- public bool IsMuted { get; private set; }
-
- public Guid? LastReadMessageId { get; private set; }
- public long LastReadSequenceId { get; private set; }
- public Guid? LastDeliveredMessageId { get; private set; }
-
- // For EF Core
- private ChatMember() : base(Guid.Empty) { Role = "member"; }
-
- internal ChatMember(Guid chatId, Guid userId, string role) : base(Guid.NewGuid())
- {
- ChatId = chatId;
- UserId = userId;
- Role = role;
- JoinedAt = DateTime.UtcNow;
- }
-
- public void TogglePin() => IsPinned = !IsPinned;
-
- public void UpdateReadCursor(Guid messageId, long sequenceId)
- {
- if (sequenceId > LastReadSequenceId)
- {
- LastReadMessageId = messageId;
- LastReadSequenceId = sequenceId;
- }
- }
-
- public void UpdateDeliveredCursor(Guid messageId)
- {
- LastDeliveredMessageId = messageId;
- }
-}
-
diff --git a/backend/src/Modules/Conversations/Domain/ChatConstants.cs b/backend/src/Modules/Conversations/Domain/ChatConstants.cs
deleted file mode 100644
index 33e0890..0000000
--- a/backend/src/Modules/Conversations/Domain/ChatConstants.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Knot.Modules.Conversations.Domain;
-
-public static class ChatConstants
-{
- public const int DefaultMessageQueryLimit = 100;
- public const int MaxSharedMediaQueryLimit = 300;
- public const int SearchMessagesLimit = 50;
- public const int MaxFileUploadSizeMb = 50;
-}
-
diff --git a/backend/src/Modules/Conversations/Domain/ChatErrors.cs b/backend/src/Modules/Conversations/Domain/ChatErrors.cs
deleted file mode 100644
index 5ae9405..0000000
--- a/backend/src/Modules/Conversations/Domain/ChatErrors.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Knot.Shared.Kernel;
-
-namespace Knot.Modules.Conversations.Domain;
-
-public static class ChatErrors
-{
- public static readonly Error FileEmpty = new Error("File.Empty", "No file uploaded");
- public static readonly Error FileInvalidExtension = new Error("File.InvalidExtension", "Must be a ZIP archive");
- public static readonly Error ImportExpired = new Error("Import.Expired", "Session not found or expired");
- public static readonly Error ImportMissing = new Error("Import.Missing", "ZIP file lost");
- public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Chat not found or access denied");
- public static readonly Error NotFound = new Error("Chat.NotFound", "Chat not found"); // Alias
- public static readonly Error NotMember = new Error("Chat.NotMember", "You are not a member of this chat");
- public static readonly Error ChatsForbidden = new Error("Chats.Forbidden", "Вы не являетесь участником этого чата.");
- public static readonly Error MessagesNotFound = new Error("Messages.NotFound", "Message not found.");
- public static readonly Error ChatsNotFound = new Error("Chats.NotFound", "Чат не найден.");
- public static readonly Error Unauthorized = new Error("Chats.Unauthorized", "Access denied");
- public static readonly Error FoldersDisabled = new Error("Folders.Disabled", "Folders feature is disabled by the administrator.");
- public static readonly Error PollsDisabled = new Error("Polls.Disabled", "Polls are disabled by the administrator.");
- public static readonly Error MediaDisabled = new Error("Media.Disabled", "Media messages are disabled by the administrator.");
-
- public static Error ImportCreateChatFailed(string msg) => new Error("Import.CreateChatFailed", msg);
- public static Error FileTooLarge(int maxMb) => new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB.");
-}
-
diff --git a/backend/src/Modules/Conversations/Domain/Folder.cs b/backend/src/Modules/Conversations/Domain/Folder.cs
deleted file mode 100644
index b9a0545..0000000
--- a/backend/src/Modules/Conversations/Domain/Folder.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Knot.Shared.Kernel;
-
-namespace Knot.Modules.Conversations.Domain;
-
-///
-/// Сущность папки для группировки чатов.
-///
-public sealed class Folder : AggregateRoot
-{
- public string Name { get; private set; }
- public string? Icon { get; private set; } // URL из хранилища
- public bool IsDefault { get; private set; }
- public FolderType Type { get; private set; }
-
- public Folder(Guid id, string name, string? icon = null, bool isDefault = false, FolderType type = FolderType.Custom)
- : base(id)
- {
- Name = name;
- Icon = icon;
- IsDefault = isDefault;
- Type = type;
- }
-
- public void Update(string name, string? icon)
- {
- if (IsDefault) throw new InvalidOperationException("Cannot rename default folders.");
- Name = name;
- Icon = icon;
- }
-}
-
-public enum FolderType
-{
- All, // Все чаты
- New, // Новые (с непрочитанными)
- Muted, // Без звука
- Custom // Пользовательская
-}
-
-///
-/// Настройки конкретного чата для конкретного пользователя.
-/// Хранятся в PostgreSQL (связь User <-> Chat).
-///
-public sealed class UserChatSettings : Entity
-{
- public Guid UserId { get; private set; }
- public Guid ChatId { get; private set; }
-
- // Список папок, в которые входит чат для этого пользователя
- private readonly List _folderIds = new();
- public IReadOnlyCollection FolderIds => _folderIds.AsReadOnly();
-
- public bool IsMuted { get; private set; }
-
- private UserChatSettings() : base(Guid.NewGuid()) { }
-
- public UserChatSettings(Guid userId, Guid chatId) : base(Guid.NewGuid())
- {
- UserId = userId;
- ChatId = chatId;
- }
-
- public static UserChatSettings Create(Guid userId, Guid chatId) => new(userId, chatId);
-
- public void AddToFolder(Guid folderId)
- {
- if (!_folderIds.Contains(folderId)) _folderIds.Add(folderId);
- }
-
- public void RemoveFromFolder(Guid folderId)
- {
- _folderIds.Remove(folderId);
- }
-
- public void SetMute(bool isMuted) => IsMuted = isMuted;
-}
-
-///
-/// Глобальные настройки папок пользователя (скрытие дефолтных и т.д.).
-/// Будет храниться в MongoDB.
-///
-public sealed class UserFolderSettings : AggregateRoot
-{
- public Guid UserId { get; private set; }
-
- // Список ID папок, которые пользователь скрыл (только для дефолтных)
- public List HiddenDefaultFolderIds { get; private set; } = new();
-
- // Список пользовательских папок (Guid созданных Folder)
- public List CustomFolderIds { get; private set; } = new();
-
- public UserFolderSettings(Guid userId) : base(Guid.NewGuid())
- {
- UserId = userId;
- }
-
- public void HideFolder(Guid folderId)
- {
- if (!HiddenDefaultFolderIds.Contains(folderId)) HiddenDefaultFolderIds.Add(folderId);
- }
-
- public void ShowFolder(Guid folderId)
- {
- HiddenDefaultFolderIds.Remove(folderId);
- }
-}
diff --git a/backend/src/Modules/Conversations/Domain/IChatRepository.cs b/backend/src/Modules/Conversations/Domain/IChatRepository.cs
deleted file mode 100644
index e2b5335..0000000
--- a/backend/src/Modules/Conversations/Domain/IChatRepository.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using Knot.Modules.Conversations.Domain;
-
-namespace Knot.Modules.Conversations.Domain;
-
-public interface IChatRepository
-{
- void Add(Chat chat);
- void Update(Chat chat);
- void Remove(Chat chat);
- Task GetByIdAsync(Guid id, CancellationToken cancellationToken);
- Task GetFavoritesAsync(Guid userId, CancellationToken cancellationToken);
- Task> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken);
-}
-
-public interface IFolderRepository
-{
- void Add(Folder folder);
- void Update(Folder folder);
- void Remove(Folder folder);
- Task GetByIdAsync(Guid id, CancellationToken cancellationToken);
- Task> GetUserFoldersAsync(Guid userId, CancellationToken cancellationToken);
-}
-
-public interface IUserChatSettingsRepository
-{
- void Add(UserChatSettings settings);
- void Update(UserChatSettings settings);
- void Remove(UserChatSettings settings);
- void RemoveRange(IEnumerable settings);
- Task GetAsync(Guid userId, Guid chatId, CancellationToken cancellationToken);
- Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
-}
-
-public interface IUserFolderSettingsRepository
-{
- Task GetByUserIdAsync(Guid userId, CancellationToken cancellationToken);
- Task UpdateAsync(UserFolderSettings settings, CancellationToken cancellationToken);
- Task RemoveByUserIdAsync(Guid userId, CancellationToken cancellationToken);
-}
diff --git a/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
index 4dd2403..22094e7 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
@@ -1,5 +1,5 @@
using Knot.Shared.Kernel;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Modules.Conversations.Infrastructure.SignalR;
using MediatR;
using Microsoft.AspNetCore.SignalR;
diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs
index 06d4c8b..4cfb2d7 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs
@@ -1,5 +1,5 @@
+using Knot.Contracts.Conversations.Domain;
using Microsoft.EntityFrameworkCore;
-using Knot.Modules.Conversations.Domain;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs
index 88a5a4b..9e022e1 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs
@@ -32,4 +32,4 @@ public class ChatsDbContextFactory : IDesignTimeDbContextFactory
optionsBuilder.UseNpgsql(connectionString);
return new ChatsDbContext(optionsBuilder.Options);
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs
index 6ec5a42..9071606 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs
@@ -4,19 +4,18 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Conversations.Infrastructure.Persistence;
-using Knot.Modules.Conversations.Application.Abstractions;
-using Knot.Modules.Conversations.Domain;
using Knot.Shared.Kernel;
using Knot.Shared.Kernel.Security;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
-using DomainChat = Knot.Modules.Conversations.Domain.Chat;
+using DomainChat = Knot.Contracts.Conversations.Domain.Chat;
namespace Knot.Modules.Conversations.Infrastructure.Persistence;
-public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
+public sealed class ChatsDbContext : DbContext, Knot.Contracts.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext
{
private readonly IMediator? _mediator;
private readonly IEncryptionService? _encryptionService;
@@ -54,6 +53,8 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli
{
builder.ToTable("Chats");
builder.HasKey(c => c.Id);
+ builder.Property(c => c.IsImporting);
+ builder.Property(c => c.ImportJobId);
builder.Property(c => c.Type).HasConversion();
builder.OwnsMany(c => c.Members, mb =>
diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs
index 6813fdd..a74c062 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs
@@ -1,4 +1,4 @@
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using MongoDB.Bson.Serialization;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs
index e1d6442..f0e46a4 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs
@@ -1,4 +1,4 @@
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using MongoDB.Driver;
namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs b/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs
index 73ceced..ca8f4e2 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs
@@ -18,4 +18,4 @@ public sealed class UserDeleterService : IUserDeleterService
{
return await _sender.Send(new DeleteUserCommand(userId), cancellationToken);
}
-}
\ No newline at end of file
+}
diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs
index ce6ecd4..88f100a 100644
--- a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs
@@ -1,9 +1,11 @@
-using Knot.Modules.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Infrastructure.SignalR;
namespace Knot.Modules.Conversations.Infrastructure.Services;
-public sealed class UserStatusService : IUserStatusService, Knot.Contracts.Conversations.Abstractions.IUserStatusService
+public sealed class UserStatusService :
+ Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService,
+ Knot.Contracts.Conversations.Abstractions.IUserStatusService
{
public bool IsUserOnline(string userId)
{
diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs
index 9175ac7..5ba8259 100644
--- a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs
@@ -8,7 +8,7 @@ using Knot.Modules.Conversations.Application.Messages.Send;
using Knot.Modules.Conversations.Application.Messages.Read;
using Knot.Modules.Conversations.Application.Messages.Delete;
using Knot.Modules.Conversations.Application.Messages.React;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using Microsoft.Extensions.Caching.Memory;
using Knot.Contracts.Auth.Domain;
diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs
index 4224841..c93b5d8 100644
--- a/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs
+++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs
@@ -5,4 +5,4 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Microsoft.AspNetCore.SignalR;
-public class MessageNotifier : IMessageNotifier { private readonly IHubContext _hubContext; public MessageNotifier(IHubContext hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
\ No newline at end of file
+public class MessageNotifier : IMessageNotifier { private readonly IHubContext _hubContext; public MessageNotifier(IHubContext hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
diff --git a/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs
index 4ddafa9..31a27ab 100644
--- a/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs
+++ b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs
@@ -26,7 +26,7 @@ namespace Knot.Modules.Conversations.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
- modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
@@ -56,9 +56,9 @@ namespace Knot.Modules.Conversations.Migrations
b.ToTable("Chats", "chats");
});
- modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
- b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
+ b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property("Id")
.ValueGeneratedOnAdd()
diff --git a/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs
new file mode 100644
index 0000000..380089d
--- /dev/null
+++ b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs
@@ -0,0 +1,166 @@
+//
+using System;
+using Knot.Modules.Conversations.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Knot.Modules.Conversations.Migrations
+{
+ [DbContext(typeof(ChatsDbContext))]
+ [Migration("20260406122703_AddIsImportingToChat")]
+ partial class AddIsImportingToChat
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("chats")
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Avatar")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("IsImporting")
+ .HasColumnType("boolean");
+
+ b.Property("LastMessageSequenceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Chats", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .HasColumnType("text");
+
+ b.Property("IsDefault")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Folders", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatId")
+ .HasColumnType("uuid");
+
+ b.Property("FolderIds")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsMuted")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ChatId")
+ .IsUnique();
+
+ b.ToTable("UserChatSettings", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
+ {
+ b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b1.Property("ChatId")
+ .HasColumnType("uuid");
+
+ b1.Property("IsMuted")
+ .HasColumnType("boolean");
+
+ b1.Property("IsPinned")
+ .HasColumnType("boolean");
+
+ b1.Property("JoinedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b1.Property("LastDeliveredMessageId")
+ .HasColumnType("uuid");
+
+ b1.Property("LastReadMessageId")
+ .HasColumnType("uuid");
+
+ b1.Property("LastReadSequenceId")
+ .HasColumnType("bigint");
+
+ b1.Property("Role")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.Property("UserId")
+ .HasColumnType("uuid");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("ChatId", "UserId")
+ .IsUnique();
+
+ b1.ToTable("ChatMembers", "chats");
+
+ b1.WithOwner()
+ .HasForeignKey("ChatId");
+ });
+
+ b.Navigation("Members");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs
new file mode 100644
index 0000000..c65ec3c
--- /dev/null
+++ b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs
@@ -0,0 +1,32 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Knot.Modules.Conversations.Migrations
+{
+ ///
+ public partial class AddIsImportingToChat : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "IsImporting",
+ schema: "chats",
+ table: "Chats",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "IsImporting",
+ schema: "chats",
+ table: "Chats");
+ }
+ }
+}
diff --git a/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs
new file mode 100644
index 0000000..bb6a049
--- /dev/null
+++ b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs
@@ -0,0 +1,169 @@
+//
+using System;
+using Knot.Modules.Conversations.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Knot.Modules.Conversations.Migrations
+{
+ [DbContext(typeof(ChatsDbContext))]
+ [Migration("20260406123444_AddImportJobIdToChat")]
+ partial class AddImportJobIdToChat
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("chats")
+ .HasAnnotation("ProductVersion", "10.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Avatar")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("ImportJobId")
+ .HasColumnType("uuid");
+
+ b.Property("IsImporting")
+ .HasColumnType("boolean");
+
+ b.Property("LastMessageSequenceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Chats", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .HasColumnType("text");
+
+ b.Property("IsDefault")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Folders", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatId")
+ .HasColumnType("uuid");
+
+ b.Property("FolderIds")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsMuted")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ChatId")
+ .IsUnique();
+
+ b.ToTable("UserChatSettings", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
+ {
+ b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
+ {
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b1.Property("ChatId")
+ .HasColumnType("uuid");
+
+ b1.Property("IsMuted")
+ .HasColumnType("boolean");
+
+ b1.Property("IsPinned")
+ .HasColumnType("boolean");
+
+ b1.Property("JoinedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b1.Property("LastDeliveredMessageId")
+ .HasColumnType("uuid");
+
+ b1.Property("LastReadMessageId")
+ .HasColumnType("uuid");
+
+ b1.Property("LastReadSequenceId")
+ .HasColumnType("bigint");
+
+ b1.Property("Role")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b1.Property("UserId")
+ .HasColumnType("uuid");
+
+ b1.HasKey("Id");
+
+ b1.HasIndex("ChatId", "UserId")
+ .IsUnique();
+
+ b1.ToTable("ChatMembers", "chats");
+
+ b1.WithOwner()
+ .HasForeignKey("ChatId");
+ });
+
+ b.Navigation("Members");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs
new file mode 100644
index 0000000..e3de486
--- /dev/null
+++ b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs
@@ -0,0 +1,31 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Knot.Modules.Conversations.Migrations
+{
+ ///
+ public partial class AddImportJobIdToChat : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "ImportJobId",
+ schema: "chats",
+ table: "Chats",
+ type: "uuid",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "ImportJobId",
+ schema: "chats",
+ table: "Chats");
+ }
+ }
+}
diff --git a/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs
index b0df6c5..1931b65 100644
--- a/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs
+++ b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs
@@ -23,7 +23,7 @@ namespace Knot.Modules.Conversations.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
- modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
@@ -38,6 +38,12 @@ namespace Knot.Modules.Conversations.Migrations
b.Property("Description")
.HasColumnType("text");
+ b.Property("ImportJobId")
+ .HasColumnType("uuid");
+
+ b.Property("IsImporting")
+ .HasColumnType("boolean");
+
b.Property("LastMessageSequenceId")
.HasColumnType("bigint");
@@ -53,9 +59,61 @@ namespace Knot.Modules.Conversations.Migrations
b.ToTable("Chats", "chats");
});
- modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b =>
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b =>
{
- b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 =>
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Icon")
+ .HasColumnType("text");
+
+ b.Property("IsDefault")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Folders", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ChatId")
+ .HasColumnType("uuid");
+
+ b.Property("FolderIds")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("IsMuted")
+ .HasColumnType("boolean");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ChatId")
+ .IsUnique();
+
+ b.ToTable("UserChatSettings", "chats");
+ });
+
+ modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b =>
+ {
+ b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 =>
{
b1.Property("Id")
.ValueGeneratedOnAdd()
diff --git a/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs
index 1d9d1fd..daa0697 100644
--- a/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs
+++ b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs
@@ -9,7 +9,7 @@ using Knot.Modules.Conversations.Application.Chats.Members;
using Knot.Modules.Conversations.Application.Chats.TogglePin;
using Knot.Modules.Conversations.Application.Chats.Update;
using Knot.Modules.Conversations.Application.DTOs;
-using Knot.Modules.Conversations.Domain;
+using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.AspNetCore.Builder;
diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs
index 7cc3719..1e67390 100644
--- a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs
+++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs
@@ -32,6 +32,18 @@ public sealed class MessageQueryService : IMessageQueryService
public async Task> GetOrphanedMessagesAsync(HashSet activeChatIds, CancellationToken cancellationToken)
{
+ if (activeChatIds == null || activeChatIds.Count == 0)
+ {
+ var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken);
+ return allMessages.Select(m => new MessageInfo(
+ m.Id,
+ m.ChatId,
+ m.State.HasFlag(MessageState.IsDeleted),
+ m is MediaMessage mm && mm.Media.Any() ? mm.Media.First().Url : null,
+ m is MediaMessage mm2 && mm2.Media.Any() ? mm2.Media.Select(media => new Contracts.Messaging.Application.Abstractions.MediaInfo(media.Url)).ToList() : null
+ )).ToList();
+ }
+
var builder = Builders.Filter;
var inFilter = builder.In(m => m.ChatId, activeChatIds);
var filter = builder.Not(inFilter);
diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs
index 3a40eea..8baab52 100644
--- a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs
+++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs
@@ -62,18 +62,22 @@ public sealed class MessageRepository : IMessageRepository
.FirstOrDefaultAsync(cancellationToken);
}
- public async Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken)
+ public async Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken)
{
var builder = Builders.Filter;
var filter = builder.Eq(m => m.ChatId, chatId);
- if (cursor.HasValue)
+ if (sequenceId.HasValue)
+ {
+ filter &= builder.Lt(m => m.SequenceId, sequenceId.Value);
+ }
+ else if (cursor.HasValue)
{
filter &= builder.Lt(m => m.CreatedAt, cursor.Value);
}
return await _messages.Find(filter)
- .SortByDescending(m => m.CreatedAt)
+ .SortByDescending(m => m.SequenceId)
.Limit(limit)
.ToListAsync(cancellationToken);
}
diff --git a/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs b/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs
index 633dc59..f8e478b 100644
--- a/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs
+++ b/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs
@@ -10,4 +10,5 @@ public interface ITelegramHtmlParser
{
Task> ParseMessagesAsync(Stream htmlStream, string baseDirInZip, CancellationToken ct = default);
Task> ExtractAllUserNamesAsync(Stream htmlStream, CancellationToken ct = default);
+ Task ExtractGroupNameAsync(Stream htmlStream, CancellationToken ct = default);
}
diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs
index da971b1..be99cb9 100644
--- a/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs
+++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs
@@ -10,6 +10,7 @@ using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using Knot.Contracts.Settings.Application.Abstractions;
using Knot.Contracts.Settings.Application.DTOs;
+using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
@@ -25,18 +26,23 @@ public record ImportConflictDto(string Type, string Message, bool Blocked);
public record AnalyzeImportResponseDto(
Guid Token,
List Names,
- List Conflicts);
+ List Conflicts,
+ int TotalMessages,
+ string? GroupName);
public record AnalyzeImportCommand(Stream FileStream, string FileName) : ICommand;
internal sealed class AnalyzeImportCommandHandler : ICommandHandler
{
private readonly ISettingsService _settingsService;
+ private readonly ITelegramHtmlParser _htmlParser;
- public AnalyzeImportCommandHandler(ISettingsService settingsService)
+ public AnalyzeImportCommandHandler(ISettingsService settingsService, ITelegramHtmlParser htmlParser)
{
_settingsService = settingsService;
+ _htmlParser = htmlParser;
}
+
public async Task> Handle(AnalyzeImportCommand request, CancellationToken cancellationToken)
{
if (request.FileStream == null || request.FileStream.Length == 0)
@@ -52,68 +58,77 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler();
-
- using (var archive = ZipFile.OpenRead(tempPath))
- {
- var htmlEntries = archive.Entries
- .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase));
-
- foreach (var entry in htmlEntries)
+ await using (var fs = new FileStream(tempPath, FileMode.Create))
{
- using var stream = entry.Open();
- var parser = new HtmlParser();
- var doc = parser.ParseDocument(stream);
+ await request.FileStream.CopyToAsync(fs, cancellationToken);
+ }
- var messageNodes = doc.QuerySelectorAll(".message");
- if (messageNodes == null)
- {
- continue;
- }
+ var names = new HashSet();
+ int totalMessages = 0;
+ string? groupName = null;
- foreach (var node in messageNodes)
+ using (var archive = ZipFile.OpenRead(tempPath))
+ {
+ var htmlEntries = archive.Entries
+ .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) &&
+ (e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase) || e.Name == "export_results.html"))
+ .OrderBy(e => e.FullName.Length)
+ .ThenBy(e => e.FullName)
+ .ToList();
+
+ foreach (var entry in htmlEntries)
{
- var fromNameNode = node.QuerySelector(".from_name");
- if (fromNameNode != null)
+ using var stream = entry.Open();
+
+ if (string.IsNullOrEmpty(groupName))
{
- var nameNodeText = (IElement)fromNameNode.Clone();
- var innerSpans = nameNodeText.QuerySelectorAll("span");
- foreach (var span in innerSpans)
- {
- span.Remove();
- }
+ groupName = await _htmlParser.ExtractGroupNameAsync(stream, cancellationToken);
+ // Reset stream position if possible? No, entry.Open() returns a new stream.
+ // But wait! ExtractGroupNameAsync consumess the stream!
+ // I'll reopen it for messages if it's the same entry.
+ }
- var name = nameNodeText.TextContent.Trim();
- if (!string.IsNullOrWhiteSpace(name))
- {
- names.Add(name);
- }
+ using var stream2 = entry.Open();
+ var messages = await _htmlParser.ParseMessagesAsync(stream2, "", cancellationToken);
+ totalMessages += messages.Count;
+
+ foreach (var m in messages)
+ {
+ if (!string.IsNullOrEmpty(m.SenderName))
+ names.Add(m.SenderName);
}
}
}
+
+ if (names.Count == 0 && totalMessages == 0)
+ {
+ return Result.Failure(new Error("TelegramImport.NoMessagesFound", "В архиве не найдено сообщений Telegram или формат HTML не распознан."));
+ }
+
+ // Анализ конфликтов политик
+ var settings = await _settingsService.GetSettingsAsync(cancellationToken);
+ var conflicts = new List();
+
+ if (!settings.Messages.AllowMedia)
+ conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true));
+
+ if (!settings.Messages.AllowVoiceMessages)
+ conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true));
+
+ if (!settings.Messages.AllowPolls)
+ conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true));
+
+ TelegramImportState.TempZips[token] = tempPath;
+
+ return Result.Success(new AnalyzeImportResponseDto(token, names.OrderBy(x => x).ToList(), conflicts, totalMessages, groupName));
+ }
+ catch (Exception ex)
+ {
+ if (File.Exists(tempPath)) File.Delete(tempPath);
+ return Result.Failure(new Error("TelegramImport.ProcessError", $"Ошибка при обработке архива: {ex.Message}"));
}
-
- // Анализ конфликтов политик
- var settings = await _settingsService.GetSettingsAsync(cancellationToken);
- var conflicts = new List();
-
- if (!settings.Messages.AllowMedia)
- conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true));
-
- if (!settings.Messages.AllowVoiceMessages)
- conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true));
-
- if (!settings.Messages.AllowPolls)
- conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true));
-
- TelegramImportState.TempZips[token] = tempPath;
-
- return Result.Success(new AnalyzeImportResponseDto(token, names.ToList(), conflicts));
}
}
diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs
index 09dd5a1..5704a2a 100644
--- a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs
+++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs
@@ -6,7 +6,8 @@ namespace Knot.Modules.TelegramImport.Application.TelegramImport.DTOs;
public record ExecuteImportRequest(
Guid Token,
Dictionary Mapping,
- string? GroupName
+ string? GroupName,
+ int TotalMessages = 0
);
diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs
index d0098d0..0330cb4 100644
--- a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs
+++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs
@@ -11,7 +11,8 @@ public record TelegramMessage(
string? ReplyToId = null,
string? ForwardedFrom = null,
List? Media = null,
- List? Reactions = null
+ List? Reactions = null,
+ long OrderIndex = 0
);
public record TelegramMedia(string FilePath, string FileName, string MimeType);
diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs
index ec4b668..c77457a 100644
--- a/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs
+++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs
@@ -1,30 +1,47 @@
using System;
using System.Collections.Generic;
+using System.IO.Compression;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Knot.Shared.Kernel;
+using Knot.Contracts.Conversations.Application.Abstractions;
+using Knot.Contracts.Conversations.Domain;
+using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Modules.TelegramImport.Infrastructure.Background;
+using Knot.Shared.Kernel;
namespace Knot.Modules.TelegramImport.Application.TelegramImport;
-public record ExecuteImportResponseDto(Guid JobId, string Status);
+public record ExecuteImportResponseDto(Guid JobId, string Status, Guid ChatId);
public record ExecuteImportCommand(
Guid CurrentUserId,
Guid Token,
+ string? GroupName,
Dictionary Mapping,
- string? GroupName
+ int TotalMessages = 0
) : ICommand;
internal sealed class ExecuteImportCommandHandler : ICommandHandler
{
private readonly TelegramImportWorker _worker;
private readonly IImportJobStore _jobStore;
+ private readonly IChatRepository _chatRepository;
+ private readonly IChatsUnitOfWork _unitOfWork;
+ private readonly ITelegramHtmlParser _htmlParser;
- public ExecuteImportCommandHandler(TelegramImportWorker worker, IImportJobStore jobStore)
+ public ExecuteImportCommandHandler(
+ TelegramImportWorker worker,
+ IImportJobStore jobStore,
+ IChatRepository chatRepository,
+ IChatsUnitOfWork unitOfWork,
+ ITelegramHtmlParser htmlParser)
{
_worker = worker;
_jobStore = jobStore;
+ _chatRepository = chatRepository;
+ _unitOfWork = unitOfWork;
+ _htmlParser = htmlParser;
}
public async Task> Handle(ExecuteImportCommand request, CancellationToken cancellationToken)
@@ -34,19 +51,66 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler(new Error("Import.Expired", "Import session expired or file not found."));
}
+ var groupName = request.GroupName;
+ if (string.IsNullOrEmpty(groupName))
+ {
+ try
+ {
+ using var archive = ZipFile.OpenRead(tempPath);
+ var firstHtml = archive.Entries.FirstOrDefault(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase));
+ if (firstHtml != null)
+ {
+ using var stream = firstHtml.Open();
+ groupName = await _htmlParser.ExtractGroupNameAsync(stream, cancellationToken);
+ }
+ }
+ catch { }
+ }
+
+ // 1. Создаем чат сразу (синхронно), чтобы пользователь его увидел
+ // 1. Создаем чат сразу (синхронно), чтобы пользователь его увидел
+ var memberIdList = request.Mapping.Values.Where(v => v != Guid.Empty).Distinct().ToList();
+ if (!memberIdList.Contains(request.CurrentUserId)) memberIdList.Add(request.CurrentUserId);
var jobId = Guid.NewGuid();
- // Ставим задачу в фоне. Worker сам удалит файл и обновит статус.
- // Мы не ждем завершения, а возвращаем JobId мгновенно.
- _ = _worker.ProcessImportAsync(request, jobId, tempPath, CancellationToken.None);
+ // Решаем какой тип чата: если 2 участника или 1 участник (Saved Messages)
+ bool isPersonal = memberIdList.Count <= 2;
+ var chatType = isPersonal ? ChatType.Personal : ChatType.Group;
+
+ string finalChatName = groupName ?? "Telegram Import";
+ if (isPersonal)
+ {
+ // Берем имя собеседника из маппинга
+ var otherUserId = memberIdList.FirstOrDefault(id => id != request.CurrentUserId);
+ var otherUserName = request.Mapping.FirstOrDefault(m => m.Value == otherUserId).Key;
+ if (!string.IsNullOrEmpty(otherUserName)) finalChatName = otherUserName;
+ }
+
+ // 2. Создание чата (в скрытом состоянии)
+ var chat = Chat.Create(finalChatName, chatType, isImporting: true, importJobId: jobId);
+ chat.AddMember(request.CurrentUserId, ChatRole.Owner);
+
+ foreach (var memberId in memberIdList)
+ {
+ if (memberId != request.CurrentUserId)
+ {
+ chat.AddMember(memberId, ChatRole.Member);
+ }
+ }
+
+ _chatRepository.Add(chat);
+ await _unitOfWork.SaveChangesAsync(cancellationToken);
+
+ // 3. Фоновая обработка сообщений
+ _ = _worker.ProcessImportAsync(request, jobId, chat.Id, tempPath, CancellationToken.None);
_jobStore.AddOrUpdate(new ImportJobInfo
{
JobId = jobId,
Status = ImportJobStatus.Queued,
- TotalMessages = 0
+ TotalMessages = request.TotalMessages
});
- return Result.Success(new ExecuteImportResponseDto(jobId, "Queued"));
+ return Result.Success(new ExecuteImportResponseDto(jobId, "Processing", chat.Id));
}
}
diff --git a/backend/src/Modules/TelegramImport/DependencyInjection.cs b/backend/src/Modules/TelegramImport/DependencyInjection.cs
index 9b5b893..cab44b9 100644
--- a/backend/src/Modules/TelegramImport/DependencyInjection.cs
+++ b/backend/src/Modules/TelegramImport/DependencyInjection.cs
@@ -1,3 +1,6 @@
+using Knot.Modules.TelegramImport.Application.Abstractions;
+using Knot.Modules.TelegramImport.Infrastructure.Background;
+using Knot.Modules.TelegramImport.Infrastructure.Parser;
using Microsoft.Extensions.DependencyInjection;
namespace Knot.Modules.TelegramImport;
@@ -6,6 +9,13 @@ public static class DependencyInjection
{
public static IServiceCollection AddTelegramImportModule(this IServiceCollection services)
{
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Register worker as itself and as a hosted service
+ services.AddSingleton();
+ services.AddHostedService(sp => sp.GetRequiredService());
+
return services;
}
}
diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs
index 58d8eb3..d616491 100644
--- a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs
+++ b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
@@ -10,6 +13,7 @@ using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
using Knot.Modules.TelegramImport.Application.Abstractions;
using Knot.Modules.TelegramImport.Application.TelegramImport;
+using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs;
using Knot.Modules.TelegramImport.Infrastructure.Background;
using Knot.Shared.Kernel.Storage;
using Microsoft.Extensions.DependencyInjection;
@@ -24,7 +28,10 @@ public class TelegramImportWorker : BackgroundService
private readonly ILogger _logger;
private readonly IImportJobStore _jobStore;
- public TelegramImportWorker(IServiceProvider serviceProvider, ILogger logger, IImportJobStore jobStore)
+ public TelegramImportWorker(
+ IServiceProvider serviceProvider,
+ ILogger logger,
+ IImportJobStore jobStore)
{
_serviceProvider = serviceProvider;
_logger = logger;
@@ -34,64 +41,203 @@ public class TelegramImportWorker : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Telegram Import Worker started.");
-
- // В реальном проекте здесь будет чтение из Channels или RabbitMQ
- // Для примера оставим заглушку цикла
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(5000, stoppingToken);
}
}
- public async Task ProcessImportAsync(ExecuteImportCommand request, Guid jobId, string zipPath, CancellationToken ct)
+ public async Task ProcessImportAsync(ExecuteImportCommand request, Guid jobId, Guid chatId, string zipPath, CancellationToken ct)
{
using var scope = _serviceProvider.CreateScope();
var parser = scope.ServiceProvider.GetRequiredService();
var msgRepo = scope.ServiceProvider.GetRequiredService();
+ var reactionRepo = scope.ServiceProvider.GetRequiredService();
var chatRepo = scope.ServiceProvider.GetRequiredService();
var uow = scope.ServiceProvider.GetRequiredService();
var fileStorage = scope.ServiceProvider.GetRequiredService();
- var jobInfo = new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Processing };
+ var jobInfo = new ImportJobInfo
+ {
+ JobId = jobId,
+ Status = ImportJobStatus.Processing,
+ TotalMessages = request.TotalMessages,
+ ProcessedMessages = 0
+ };
_jobStore.AddOrUpdate(jobInfo);
try
{
- using var archive = ZipFile.OpenRead(zipPath);
- var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList();
+ var chat = await chatRepo.GetByIdAsync(chatId, ct);
+ if (chat == null) throw new Exception("Chat not found");
- // 1. Создание чата (уже было в оригинале, но здесь в фоне)
- Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга
+ _logger.LogInformation("Processing messages for ChatId: {ChatId}, JobId: {JobId}", chatId, jobId);
- foreach (var entry in entries)
+ // TRY TO FIND THE BEST ENCODING (UTF8 or CP866)
+ ZipArchive archive;
+ try
{
- using var stream = entry.Open();
- var messages = await parser.ParseMessagesAsync(stream, "", ct);
-
- foreach (var m in messages)
+ // Try UTF8 first
+ archive = ZipFile.OpenRead(zipPath);
+ var messagesHtml = archive.Entries.FirstOrDefault(e => e.Name.Equals("messages.html", StringComparison.OrdinalIgnoreCase));
+ if (messagesHtml == null)
{
- Guid senderGuid = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId;
-
- var textMsg = new TextMessage(Guid.NewGuid(), targetChatId, senderGuid, m.Content, null, null, null, m.CreatedAt, true);
- msgRepo.Add(textMsg);
-
- jobInfo.ProcessedMessages++;
- _jobStore.AddOrUpdate(jobInfo);
+ // If not found in root, maybe it's CP866
+ archive.Dispose();
+ archive = ZipFile.Open(zipPath, ZipArchiveMode.Read, Encoding.GetEncoding(866));
}
- await uow.SaveChangesAsync(ct);
+ }
+ catch
+ {
+ archive = ZipFile.OpenRead(zipPath);
}
- jobInfo.Status = ImportJobStatus.Completed;
+ using (archive)
+ {
+ var entries = archive.Entries
+ .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) &&
+ (e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase) ||
+ e.Name.Equals("export_results.html", StringComparison.OrdinalIgnoreCase)))
+ .OrderBy(e => e.FullName.Length)
+ .ThenBy(e => e.FullName)
+ .ToList();
+
+ var allMessages = new List();
+
+ foreach (var entry in entries)
+ {
+ var entryPath = entry.FullName.Replace("\\", "/");
+ var lastSlash = entryPath.LastIndexOf('/');
+ var baseDir = lastSlash >= 0 ? entryPath.Substring(0, lastSlash + 1) : "";
+
+ using var stream = entry.Open();
+ var messages = await parser.ParseMessagesAsync(stream, baseDir, ct);
+ allMessages.AddRange(messages);
+ }
+
+ var orderedMessages = allMessages
+ .OrderBy(m => m.CreatedAt)
+ .ThenBy(m => m.OrderIndex)
+ .ToList();
+
+ _logger.LogInformation("Total messages parsed: {Count}", orderedMessages.Count);
+
+ var telegramToKnotIdMap = new Dictionary();
+ var processedCount = 0;
+
+ foreach (var m in orderedMessages)
+ {
+ Guid senderId = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId;
+ Guid? replyToId = m.ReplyToId != null && telegramToKnotIdMap.TryGetValue(m.ReplyToId, out var rid) ? rid : null;
+
+ Guid knotMsgId = Guid.NewGuid();
+ Message? knotMsg = null;
+
+ if (m.Media != null && m.Media.Any())
+ {
+ var mediaType = m.Media.Any(mi => mi.MimeType.StartsWith("image")) ? MediaType.Image :
+ m.Media.Any(mi => mi.MimeType.StartsWith("video")) ? MediaType.Video :
+ m.Media.Any(mi => mi.MimeType.Contains("voice")) ? MediaType.Voice : MediaType.File;
+
+ var mediaMsg = new MediaMessage(knotMsgId, chat.Id, senderId, mediaType, m.Content, replyToId, null, m.CreatedAt, true);
+
+ foreach (var mediaItem in m.Media)
+ {
+ var targetPath = mediaItem.FilePath.Replace("\\", "/").TrimStart('/');
+ var unescapedPath = Uri.UnescapeDataString(targetPath);
+
+ var zipEntry = archive.Entries.FirstOrDefault(e => {
+ var entryPath = e.FullName.Replace("\\", "/").TrimStart('/');
+ return entryPath.Equals(targetPath, StringComparison.OrdinalIgnoreCase) ||
+ entryPath.Equals(unescapedPath, StringComparison.OrdinalIgnoreCase);
+ });
+
+ // IF NOT FOUND, try a desperate match by stripping path and comparing filenames
+ if (zipEntry == null)
+ {
+ var fileName = Path.GetFileName(targetPath);
+ zipEntry = archive.Entries.FirstOrDefault(e => e.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (zipEntry != null)
+ {
+ using var mediaStream = zipEntry.Open();
+ string itemType = "file";
+ string mimeToUpload = mediaItem.MimeType;
+
+ if (mediaItem.MimeType.Equals("image/gif", StringComparison.OrdinalIgnoreCase))
+ {
+ itemType = "gif";
+ if (mediaItem.FileName.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)) mimeToUpload = "video/mp4";
+ }
+ else if (mediaItem.MimeType.StartsWith("image")) itemType = "image";
+ else if (mediaItem.MimeType.StartsWith("video")) itemType = "video";
+ else if (mediaItem.MimeType.Contains("voice") || mediaItem.MimeType.Contains("audio")) itemType = "audio";
+
+ var fileId = await fileStorage.UploadFileAsync(mediaStream, mediaItem.FileName, mimeToUpload);
+ mediaMsg.AddMedia(itemType, $"/api/files/{fileId}", mediaItem.FileName, zipEntry.Length);
+ }
+ else
+ {
+ _logger.LogWarning("[Import] Media entry NOT FOUND in ZIP: {Path}", targetPath);
+ }
+ }
+
+ if (mediaMsg.Media.Any()) knotMsg = mediaMsg;
+ }
+
+ if (knotMsg == null)
+ {
+ knotMsg = new TextMessage(knotMsgId, chat.Id, senderId, m.Content, replyToId, null, null, m.CreatedAt, true);
+ }
+
+ chat.IncrementSequenceId();
+ knotMsg.SetSequenceId(chat.LastMessageSequenceId);
+ msgRepo.Add(knotMsg);
+
+ if (m.Reactions != null)
+ {
+ foreach (var r in m.Reactions)
+ {
+ foreach (var name in r.UserNames)
+ {
+ var userId = request.Mapping.TryGetValue(name, out var mappedId) && mappedId != Guid.Empty ? mappedId : request.CurrentUserId;
+ var knotReaction = new MessageReaction(knotMsgId, userId, r.Emoji);
+ await reactionRepo.AddAsync(knotReaction, ct);
+ }
+ }
+ }
+
+ telegramToKnotIdMap[m.Id] = knotMsgId;
+ processedCount++;
+
+ if (processedCount % 50 == 0)
+ {
+ await uow.SaveChangesAsync(ct);
+ jobInfo.ProcessedMessages = processedCount;
+ _jobStore.AddOrUpdate(jobInfo);
+ }
+ }
+
+ chat.CompleteImport();
+ await uow.SaveChangesAsync(ct);
+
+ _logger.LogInformation("Import Completed. Messages: {Count}", processedCount);
+ jobInfo.ProcessedMessages = processedCount;
+ jobInfo.Status = ImportJobStatus.Completed;
+ }
}
catch (Exception ex)
{
+ _logger.LogError(ex, "Error during Telegram import process.");
jobInfo.Status = ImportJobStatus.Failed;
jobInfo.ErrorMessage = ex.Message;
}
finally
{
_jobStore.AddOrUpdate(jobInfo);
- try { File.Delete(zipPath); } catch { }
+ try { if (File.Exists(zipPath)) File.Delete(zipPath); } catch { }
+ TelegramImportState.TempZips.TryRemove(request.Token, out _);
}
}
}
diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs
index 8df23eb..9883e5e 100644
--- a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs
+++ b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs
@@ -8,16 +8,19 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
namespace Knot.Modules.TelegramImport.Infrastructure.Parser;
public sealed class TelegramHtmlParser : ITelegramHtmlParser
{
private readonly HtmlParser _parser;
+ private readonly ILogger _logger;
- public TelegramHtmlParser()
+ public TelegramHtmlParser(ILogger logger)
{
_parser = new HtmlParser();
+ _logger = logger;
}
public async Task> ExtractAllUserNamesAsync(Stream htmlStream, CancellationToken ct = default)
@@ -39,42 +42,207 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser
{
var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
var messages = new List();
+ string? lastSenderName = null;
var messageNodes = doc.QuerySelectorAll(".message");
foreach (var node in messageNodes)
{
- var msg = ParseSingleMessage(node, baseDirInZip);
+ // STOP ALL MERGING OF SEPARATE NODES.
+ // Telegram HTML nodes represent visual bubbles. If we merge them, we break the original hierarchy
+ // and often misplace captions or hide media.
+ var msg = ParseSingleMessage(node, baseDirInZip, ref lastSenderName);
if (msg != null) messages.Add(msg);
}
return messages;
}
- private TelegramMessage? ParseSingleMessage(IElement node, string baseDir)
+ public async Task ExtractGroupNameAsync(Stream htmlStream, CancellationToken ct = default)
{
+ var doc = await _parser.ParseDocumentAsync(htmlStream, ct);
+ var header = doc.QuerySelector(".page_header .text");
+ var name = header?.TextContent?.Trim();
+
+ if (string.IsNullOrEmpty(name))
+ {
+ name = doc.Title?.Replace("Chat Export with ", "")?.Trim();
+ }
+
+ return name;
+ }
+
+ private TelegramMessage? ParseSingleMessage(IElement node, string baseDir, ref string? lastSenderName)
+ {
+ if (node.ClassList.Contains("service")) return null;
+
try
{
- var id = node.GetAttribute("id") ?? Guid.NewGuid().ToString();
- var fromNameNode = node.QuerySelector(".from_name");
- var senderName = fromNameNode != null ? CleanName(fromNameNode) : null;
-
+ var idAttr = node.GetAttribute("id") ?? Guid.NewGuid().ToString();
+ long numericId = 0;
+ if (idAttr.StartsWith("message")) long.TryParse(idAttr.Replace("message", ""), out numericId);
+
+ var fromNameNode = node.QuerySelector(".body > .from_name");
+ // Ignore forwarded from_name as message sender
+ if (fromNameNode != null && fromNameNode.Closest(".forwarded") != null) fromNameNode = null;
+
+ var senderName = fromNameNode != null ? CleanName(fromNameNode) : lastSenderName;
+ // If it's a new bubble group (no "joined"), update lastSenderName
+ if (senderName != null && !node.ClassList.Contains("joined")) lastSenderName = senderName;
+
var textNode = node.QuerySelector(".text");
var content = textNode?.TextContent?.Trim() ?? "";
- // Дата (парсинг из title)
var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]");
var dateStr = dateNode?.GetAttribute("title") ?? "";
- DateTime.TryParse(dateStr.Replace("UTC", "").Trim(), out var createdAt);
+ DateTime createdAt = DateTime.UtcNow;
- return new TelegramMessage(id, senderName, createdAt, content);
+ if (!string.IsNullOrEmpty(dateStr))
+ {
+ var cleanDateStr = dateStr.Trim();
+ var formats = new[] { "dd.MM.yyyy HH:mm:ss 'UTC'zzz", "dd.MM.yyyy HH:mm:ss", "d.MM.yyyy HH:mm:ss", "dd.M.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss" };
+
+ try
+ {
+ if (DateTimeOffset.TryParseExact(cleanDateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto))
+ {
+ createdAt = dto.UtcDateTime;
+ }
+ else if (DateTimeOffset.TryParse(cleanDateStr.Replace("UTC", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto2))
+ {
+ createdAt = dto2.UtcDateTime;
+ }
+ }
+ catch { }
+ }
+
+ var mediaList = ExtractMedia(node, baseDir);
+
+ // Replies
+ string? replyToId = null;
+ var replyNode = node.QuerySelector(".reply_to a[href^=\"#go_to_message\"]");
+ if (replyNode != null)
+ {
+ var href = replyNode.GetAttribute("href");
+ replyToId = href?.Replace("#go_to_message", "message");
+ }
+
+ // Forwards
+ string? forwardedFrom = null;
+ var forwardNode = node.QuerySelector(".forwarded .from_name");
+ if (forwardNode != null) forwardedFrom = CleanName(forwardNode);
+
+ // Reactions
+ var reactions = new List();
+ var reactionNodes = node.QuerySelectorAll(".reactions .reaction");
+ foreach (var r in reactionNodes)
+ {
+ var emoji = r.QuerySelector(".emoji")?.TextContent?.Trim() ?? "";
+ var names = r.QuerySelectorAll(".userpics [title]")
+ .Select(x => x.GetAttribute("title") ?? "")
+ .Where(x => !string.IsNullOrEmpty(x))
+ .ToList();
+ if (!string.IsNullOrEmpty(emoji)) reactions.Add(new TelegramReaction(emoji, names));
+ }
+
+ return new TelegramMessage(idAttr, senderName, createdAt, content, replyToId, forwardedFrom, mediaList.Any() ? mediaList : null, reactions.Any() ? reactions : null, numericId);
}
- catch { return null; }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error parsing single Telegram message");
+ return null;
+ }
+ }
+
+ private List ExtractMedia(IElement node, string baseDir)
+ {
+ var mediaList = new List();
+
+ // Look for links that represent media containers
+ var allLinks = node.QuerySelectorAll("a[href]").ToList();
+
+ foreach (var link in allLinks)
+ {
+ var href = NormalizeHref(link.GetAttribute("href"));
+ if (href == null || !href.Contains("/")) continue;
+
+ // Skip internal pages
+ if (href.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) continue;
+
+ var fullPath = BuildPath(baseDir, href);
+ if (mediaList.Any(m => m.FilePath == fullPath)) continue;
+
+ var fileName = Path.GetFileName(href);
+ var mimeType = GetMimeType(href);
+
+ // ANIMATION DETECTION:
+ // 1. Check classes (standard TG export)
+ bool hasGifClass = link.ClassList.Contains("animated_wrap") || link.ClassList.Contains("media_video");
+
+ // 2. Check full text of the link container for "Animation" string
+ // This is the most reliable way to find TG "GIFs" which are actually MP4s
+ bool hasAnimationText = link.TextContent.Contains("Animation", StringComparison.OrdinalIgnoreCase);
+
+ if (hasGifClass || hasAnimationText)
+ {
+ mimeType = "image/gif";
+ }
+
+ if (link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages"))
+ {
+ mimeType = "audio/ogg";
+ }
+
+ mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType));
+ }
+
+ // Alternative check for voice messages in audio tags
+ foreach (var audioEl in node.QuerySelectorAll("audio[src]"))
+ {
+ var href = NormalizeHref(audioEl.GetAttribute("src"));
+ if (href == null) continue;
+ var fullPath = BuildPath(baseDir, href);
+ if (!mediaList.Any(m => m.FilePath == fullPath))
+ mediaList.Add(new TelegramMedia(fullPath, Path.GetFileName(href), "audio/ogg"));
+ }
+
+ return mediaList;
+ }
+
+ private static string? NormalizeHref(string? href)
+ {
+ if (string.IsNullOrWhiteSpace(href)) return null;
+ if (href.Contains('#')) href = href[..href.IndexOf('#')];
+ if (href.Contains('?')) href = href[..href.IndexOf('?')];
+ return string.IsNullOrWhiteSpace(href) ? null : href.Replace("\\", "/");
+ }
+
+ private static string BuildPath(string baseDir, string href)
+ {
+ var normalized = href.Replace("\\", "/");
+ return normalized.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase)
+ ? normalized
+ : (baseDir.TrimEnd('/') + "/" + normalized.TrimStart('/')).Replace("//", "/");
}
private string CleanName(IElement node)
{
var clone = (IElement)node.Clone();
- foreach (var span in clone.QuerySelectorAll("span")) span.Remove();
+ foreach (var span in clone.QuerySelectorAll("span, a, div")) span.Remove();
return clone.TextContent.Trim();
}
+
+ private string GetMimeType(string path)
+ {
+ var ext = Path.GetExtension(path).ToLowerInvariant();
+ return ext switch
+ {
+ ".jpg" or ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".webp" => "image/webp",
+ ".mp4" => "video/mp4",
+ ".mov" => "video/quicktime",
+ ".webm" => "video/webm",
+ _ => "application/octet-stream"
+ };
+ }
}
diff --git a/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs
index fc47191..b304c5d 100644
--- a/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs
+++ b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs
@@ -1,5 +1,6 @@
using Knot.Modules.TelegramImport.Application.TelegramImport;
using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs;
+using Knot.Modules.TelegramImport.Infrastructure.Background;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.AspNetCore.Builder;
@@ -25,14 +26,23 @@ public static class TelegramImportEndpoints
using var stream = file.OpenReadStream();
var result = await sender.Send(new AnalyzeImportCommand(stream, file.FileName), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code);
- }).DisableAntiforgery();
+ }).DisableAntiforgery().WithMetadata(new Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute(1000000000));
group.MapPost("execute", async ([FromBody] ExecuteImportRequest req, ISender sender, IUserContext userContext, CancellationToken ct) =>
{
- var command = new ExecuteImportCommand(userContext.UserId, req.Token, req.Mapping, req.GroupName);
+ var command = new ExecuteImportCommand(userContext.UserId, req.Token, req.GroupName, req.Mapping, req.TotalMessages);
var result = await sender.Send(command, ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code);
});
+
+ group.MapGet("status/{jobId:guid}", (Guid jobId, IImportJobStore jobStore) =>
+ {
+ if (jobStore.TryGet(jobId, out var info))
+ {
+ return Results.Ok(info);
+ }
+ return Results.NotFound();
+ });
}
}
diff --git a/client-web/src/core/domain/types.ts b/client-web/src/core/domain/types.ts
index 899db30..a2e8df5 100644
--- a/client-web/src/core/domain/types.ts
+++ b/client-web/src/core/domain/types.ts
@@ -124,6 +124,8 @@ export interface Chat {
members: ChatMember[];
messages: Message[];
unreadCount: number;
+ isImporting?: boolean;
+ importJobId?: string | null;
pinnedMessages?: Array<{
id: string;
message: Message;
diff --git a/client-web/src/core/infrastructure/i18n.ts b/client-web/src/core/infrastructure/i18n.ts
index 8773627..0c4a259 100644
--- a/client-web/src/core/infrastructure/i18n.ts
+++ b/client-web/src/core/infrastructure/i18n.ts
@@ -44,6 +44,21 @@ const translations = {
storageAndData: 'Хранилище и данные',
importTelegram: 'Импорт истории Telegram',
importTelegramDesc: 'Перенести сообщения и медиа из Telegram',
+ importSuccess: 'Импорт завершен',
+ importStarted: 'Импорт запущен в фоновом режиме',
+ importSelectArchive: 'Загрузите архив',
+ importSelectArchiveDesc: 'Экспортируйте чат из Telegram (HTML формат, вкл. медиа) и загрузите полученный ZIP архив.',
+ importLimit: 'Лимит: до 1 ГБ',
+ importSelectFile: 'Выбрать ZIP-архив',
+ importParticipants: 'Соответствие участников',
+ importParticipantsDesc: 'Назначьте участников из архива пользователям в Knot.',
+ importInArchive: 'В архиве',
+ importSelectContact: '-- Выберите контакт --',
+ importGroupName: 'Название чата',
+ importGroupNameHint: 'Например, Моя группа',
+ importStart: 'Запустить импорт',
+ importRestart: 'Начать заново',
+ importCompletedDesc: 'Мы успешно создали чат и начали перенос сообщений в фоновом режиме.',
// Chat
chat: 'Чат',
group: 'Группа',
@@ -413,6 +428,21 @@ const translations = {
storageAndData: 'Storage and Data',
importTelegram: 'Import Telegram History',
importTelegramDesc: 'Move messages and media from Telegram',
+ importSuccess: 'Import completed',
+ importStarted: 'Import started in background',
+ importSelectArchive: 'Upload archive',
+ importSelectArchiveDesc: 'Export chat from Telegram (HTML format, incl. media) and upload the resulting ZIP archive.',
+ importLimit: 'Limit: up to 1 GB',
+ importSelectFile: 'Select ZIP archive',
+ importParticipants: 'Participant mapping',
+ importParticipantsDesc: 'Assign participants from the archive to Knot users.',
+ importInArchive: 'In archive',
+ importSelectContact: '-- Select contact --',
+ importGroupName: 'Chat name',
+ importGroupNameHint: 'e.g., My Group',
+ importStart: 'Start import',
+ importRestart: 'Start over',
+ importCompletedDesc: 'We successfully created the chat and started importing messages in the background.',
searchChats: 'Search chats...',
chat: 'Chat',
group: 'Group',
diff --git a/client-web/src/core/presentation/components/ui/Avatar.tsx b/client-web/src/core/presentation/components/ui/Avatar.tsx
index 37e0d2f..8738ce3 100644
--- a/client-web/src/core/presentation/components/ui/Avatar.tsx
+++ b/client-web/src/core/presentation/components/ui/Avatar.tsx
@@ -46,7 +46,7 @@ function AvatarInner({ src, name, size = 'md', className = '', online }: AvatarP
const gradientClass = generateAvatarColor(name || '');
return (
-
+
{src ? (
) : (
{initials}
diff --git a/client-web/src/core/presentation/components/ui/ConfirmModal.tsx b/client-web/src/core/presentation/components/ui/ConfirmModal.tsx
index 67cffc5..660927d 100644
--- a/client-web/src/core/presentation/components/ui/ConfirmModal.tsx
+++ b/client-web/src/core/presentation/components/ui/ConfirmModal.tsx
@@ -1,5 +1,6 @@
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle } from 'lucide-react';
+import { createPortal } from 'react-dom';
import { useLang } from '../../../infrastructure/i18n';
interface ConfirmModalProps {
@@ -25,49 +26,50 @@ export default function ConfirmModal({
}: ConfirmModalProps) {
const { t } = useLang();
- return (
+ const content = (
{open && (
{ if (e.target === e.currentTarget) onCancel(); }}
>
-
-
-
+
+
{title && (
-
{title}
+
{title}
)}
-
{message}
+
+ {message}
+
-
+
+
{cancelText || t('cancel')}
-
{confirmText || t('confirm')}
@@ -78,4 +80,6 @@ export default function ConfirmModal({
)}
);
+
+ return createPortal(content, document.body);
}
diff --git a/client-web/src/core/presentation/layouts/SideMenu.tsx b/client-web/src/core/presentation/layouts/SideMenu.tsx
index 4a373c4..fabd97e 100644
--- a/client-web/src/core/presentation/layouts/SideMenu.tsx
+++ b/client-web/src/core/presentation/layouts/SideMenu.tsx
@@ -37,7 +37,6 @@ import { getSocket } from '../../infrastructure/socket';
import { useLang } from '../../infrastructure/i18n';
import { useThemeStore, ChatTheme } from '../../application/stores/themeStore';
import DatePicker from '../components/ui/DatePicker';
-import TelegramImportModal from '../../../modules/users/presentation/components/TelegramImportModal';
import type { User as UserType, UserPresence, FriendRequest, FriendWithId } from '../../domain/types';
import { getInitials } from '../../utils/utils';
@@ -48,9 +47,10 @@ interface SideMenuProps {
isOpen: boolean;
onClose: () => void;
onOpenProfile: () => void;
+ onOpenTelegramImport: () => void;
}
-export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuProps) {
+export default function SideMenu({ isOpen, onClose, onOpenProfile, onOpenTelegramImport }: SideMenuProps) {
const { user, updateUser, logout } = useAuthStore();
const { clearStore } = useChatStore();
const { chatTheme, setChatTheme } = useThemeStore();
@@ -60,8 +60,6 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
const [prevView, setPrevView] = useState('main');
const [themeIndex, setThemeIndex] = useState(0);
const fileInputRef = useRef(null);
-
- const [showImportModal, setShowImportModal] = useState(false);
// Friends state
const {
@@ -325,7 +323,7 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
Хранилище и данные
{ loadFriends(); setShowImportModal(true); }}
+ onClick={() => { onOpenTelegramImport(); }}
className="w-full flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50 hover:bg-surface-hover transition-colors"
>
@@ -674,7 +672,6 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
{view === 'about' && renderAbout()}
-
setShowImportModal(false)} friends={friends} />
>
)}
diff --git a/client-web/src/core/presentation/layouts/Sidebar.tsx b/client-web/src/core/presentation/layouts/Sidebar.tsx
index 2e5a064..bf6ffd7 100644
--- a/client-web/src/core/presentation/layouts/Sidebar.tsx
+++ b/client-web/src/core/presentation/layouts/Sidebar.tsx
@@ -12,6 +12,7 @@ import { useAuthStore } from '../../../modules/auth/application/authStore';
import { useChatStore } from '../../../modules/chats/application/chatStore';
import { useNotificationStore } from '../../application/stores/notificationStore';
import { useLang } from '../../infrastructure/i18n';
+import { useFriendStore } from '../../../modules/friends/application/friendStore';
import { StoryApi } from '../../../modules/stories/infrastructure/storyApi';
import { getSocket } from '../../infrastructure/socket';
import { getInitials, generateAvatarColor } from '../../utils/utils';
@@ -24,6 +25,7 @@ import SideMenu from './SideMenu';
import StoryViewer from '../../../modules/stories/presentation/components/StoryViewer';
import { CreateStoryModal } from '../../../modules/stories/presentation/components/CreateStoryModal';
import { useStoryStore } from '../../../modules/stories/application/storyStore';
+import TelegramImportModal from '../../../modules/users/presentation/components/TelegramImportModal';
const API_URL = import.meta.env.VITE_API_URL || '';
@@ -36,6 +38,8 @@ export default function Sidebar() {
const [showSideMenu, setShowSideMenu] = useState(false);
const { storyGroups, setStoryGroups, viewerIndex, viewerStoryIndex, openViewer, closeViewer } = useStoryStore();
const [showCreateStory, setShowCreateStory] = useState(false);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const { friends, loadFriends } = useFriendStore();
const loadStories = () => {
StoryApi.getStories()
@@ -63,14 +67,17 @@ export default function Sidebar() {
const handleOpenNewChat = () => setShowNewChat(true);
const handleOpenSideMenu = () => setShowSideMenu(true);
+ const handleOpenImport = () => { loadFriends(); setShowImportModal(true); };
window.addEventListener('OPEN_NEW_CHAT', handleOpenNewChat);
window.addEventListener('OPEN_SIDE_MENU', handleOpenSideMenu);
+ window.addEventListener('OPEN_TELEGRAM_IMPORT', handleOpenImport);
return () => {
clearInterval(interval);
socket?.off('story_viewed', onStoryViewed);
window.removeEventListener('OPEN_NEW_CHAT', handleOpenNewChat);
window.removeEventListener('OPEN_SIDE_MENU', handleOpenSideMenu);
+ window.removeEventListener('OPEN_TELEGRAM_IMPORT', handleOpenImport);
};
}, [user?.id]);
@@ -80,9 +87,9 @@ export default function Sidebar() {
if (chat.name?.toLowerCase().includes(q)) return true;
return chat.members.some(
(m) =>
- m.user.id !== user?.id &&
- ((m.user.username || m.user.userName || '').toLowerCase().includes(q) ||
- (m.user.displayName || '').toLowerCase().includes(q))
+ m.user?.id !== user?.id &&
+ ((m.user?.username || m.user?.userName || '').toLowerCase().includes(q) ||
+ (m.user?.displayName || '').toLowerCase().includes(q))
);
}).sort((a, b) => {
// 1. Favorites chat always on top
@@ -90,13 +97,19 @@ export default function Sidebar() {
if (b.type === 'favorites') return 1;
// 2. Pinned chats next
- const aPinned = a.members.find(m => m.user.id === user?.id)?.isPinned ?? false;
- const bPinned = b.members.find(m => m.user.id === user?.id)?.isPinned ?? false;
+ const aPinned = a.members?.find(m => m.user?.id === user?.id)?.isPinned ?? false;
+ const bPinned = b.members?.find(m => m.user?.id === user?.id)?.isPinned ?? false;
if (aPinned && !bPinned) return -1;
if (!aPinned && bPinned) return 1;
- // 3. Last message timestamp (if available) - though currently we don't have it on top level
- return 0;
+ // 3. Importing chats next
+ if (a.isImporting && !b.isImporting) return -1;
+ if (!a.isImporting && b.isImporting) return 1;
+
+ // 4. Last message timestamp or CreatedAt
+ const aTime = new Date(a.messages?.[0]?.createdAt || a.createdAt).getTime();
+ const bTime = new Date(b.messages?.[0]?.createdAt || b.createdAt).getTime();
+ return bTime - aTime;
});
const handleLogout = () => {
@@ -214,7 +227,7 @@ export default function Sidebar() {
{/* Модалки */}
- {showNewChat && setShowNewChat(false)} />}
+ {showNewChat && setShowNewChat(false)} onOpenTelegramImport={() => { loadFriends(); setShowImportModal(true); }} />}
{showProfile && user && setShowProfile(false)} isSelf />}
@@ -223,6 +236,12 @@ export default function Sidebar() {
isOpen={showSideMenu}
onClose={() => setShowSideMenu(false)}
onOpenProfile={() => setShowProfile(true)}
+ onOpenTelegramImport={() => { loadFriends(); setShowImportModal(true); }}
+ />
+ setShowImportModal(false)}
+ friends={friends}
/>
{viewerIndex !== null && storyGroups.length > 0 && (
diff --git a/client-web/src/modules/chats/application/chatStore.ts b/client-web/src/modules/chats/application/chatStore.ts
index 0108d4b..8f80030 100644
--- a/client-web/src/modules/chats/application/chatStore.ts
+++ b/client-web/src/modules/chats/application/chatStore.ts
@@ -123,7 +123,7 @@ export const useChatStore = create((set, get) => ({
set({ isLoadingMessages: true });
const currentMessages = state.messages[chatId] || [];
- const cursor = !reset && currentMessages.length > 0 ? currentMessages[0].createdAt : undefined;
+ const cursor = !reset && currentMessages.length > 0 ? currentMessages[0].sequenceId.toString() : undefined;
const fetched = await ChatApi.getMessages(chatId, cursor);
@@ -132,12 +132,13 @@ export const useChatStore = create((set, get) => ({
const existing = reset ? [] : (state.messages[chatId] || []);
const fetchedIds = new Set(fetched.map(m => m.id));
const socketOnly = existing.filter(m => !fetchedIds.has(m.id));
- const merged = [...fetched, ...socketOnly].sort(
- (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
- );
+ const merged = [...fetched, ...socketOnly].sort((a, b) => {
+ const tDiff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
+ return tDiff !== 0 ? tDiff : a.sequenceId - b.sequenceId;
+ });
return {
messages: { ...state.messages, [chatId]: merged },
- hasMoreMessages: { ...state.hasMoreMessages, [chatId]: fetched.length === 100 },
+ hasMoreMessages: { ...state.hasMoreMessages, [chatId]: fetched.length >= 50 },
isLoadingMessages: false,
};
});
diff --git a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx
index 3bb4fbd..b88f039 100644
--- a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx
+++ b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx
@@ -4,6 +4,7 @@ import { ru, enUS } from 'date-fns/locale';
import { Check, CheckCheck, Image, FileText, Mic, Video, Pin, Trash2, Bookmark } from 'lucide-react';
import { useAuthStore } from '../../../auth/application/authStore';
import { useChatStore } from '../../application/chatStore';
+import { httpClient } from '../../../../core/infrastructure/httpClient';
import { useLang } from '../../../../core/infrastructure/i18n';
import { stripMarkdown } from '../../../../core/utils/utils';
import { ChatApi } from '../../infrastructure/chatApi';
@@ -72,20 +73,54 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
: lastMessage.content || ''
: '';
- const previewText = stripMarkdown(lastMessageText);
+ const previewText = chat.isImporting ? 'Импорт...' : stripMarkdown(lastMessageText);
- const isMine = lastMessage?.senderId === user?.id;
+ const isMine = !chat.isImporting && lastMessage?.senderId === user?.id;
// Галочки прочтения
- const isRead = lastMessage?.readBy?.some((r) => r.userId !== user?.id);
+ const isRead = !chat.isImporting && lastMessage?.readBy?.some((r) => r.userId !== user?.id);
- const timeStr = lastMessage
+ const timeStr = !chat.isImporting && lastMessage
? formatDistanceToNow(new Date(lastMessage.createdAt), { addSuffix: false, locale: lang === 'ru' ? ru : enUS })
: '';
const [showAttachmentConfirm, setShowAttachmentConfirm] = useState(false);
+ const [importStatus, setImportStatus] = useState<{ processed: number, total: number } | null>(null);
+
+ useEffect(() => {
+ if (!chat.isImporting || !chat.importJobId) {
+ setImportStatus(null);
+ return;
+ }
+
+ const poll = async () => {
+ try {
+ const data = await httpClient.request(`/import/telegram/status/${chat.importJobId}`);
+ setImportStatus({ processed: data.processedMessages, total: data.totalMessages });
+ if (data.status === 'Completed' || data.status === 'Failed') {
+ loadChats();
+ }
+ } catch (e: any) {
+ if (e.status === 404) {
+ // Job might have expired or backend restarted
+ console.warn('Import job not found');
+ } else {
+ console.error('Failed to poll status', e);
+ }
+ }
+ };
+
+ poll();
+ const interval = setInterval(poll, 1500);
+ return () => clearInterval(interval);
+ }, [chat.isImporting, chat.importJobId, loadChats]);
const handleClick = () => {
+ if (chat.isImporting) {
+ // Here we could show a progress modal, but for now just select it
+ setActiveChat(chat.id);
+ return;
+ }
if ((window as any).hasUnsavedAttachments && !isActive) {
setShowAttachmentConfirm(true);
return;
@@ -188,9 +223,24 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
)}
-
- {isTyping ? t('typing') : draft ? <>{t('draft')} {stripMarkdown(draft)}> : previewText}
-
+
+
+ {isTyping ? t('typing') : draft ? <>{t('draft')} {stripMarkdown(draft)}> : previewText}
+ {chat.isImporting && importStatus && importStatus.total > 0 && (
+
+ {importStatus.processed} / {importStatus.total}
+
+ )}
+
+ {chat.isImporting && importStatus && importStatus.total > 0 && (
+
+ )}
+
{chat.unreadCount > 0 && !isActive && (
diff --git a/client-web/src/modules/chats/presentation/components/ChatView.tsx b/client-web/src/modules/chats/presentation/components/ChatView.tsx
index 4425557..6387b89 100644
--- a/client-web/src/modules/chats/presentation/components/ChatView.tsx
+++ b/client-web/src/modules/chats/presentation/components/ChatView.tsx
@@ -21,6 +21,7 @@ import {
Pencil,
} from 'lucide-react';
import { useChatStore } from '../../application/chatStore';
+import { httpClient } from '../../../../core/infrastructure/httpClient';
import { useAuthStore } from '../../../auth/application/authStore';
import { ChatApi } from '../../infrastructure/chatApi';
import { getSocket } from '../../../../core/infrastructure/socket';
@@ -52,6 +53,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
hasMoreMessages,
loadMessages,
setActiveChat,
+ loadChats,
} = useChatStore();
const [showTopMenu, setShowTopMenu] = useState(false);
@@ -84,6 +86,36 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const chatMessages = allChatMessages.filter(m => !m.isDeleted);
const pinnedMsg = activeChat ? pinnedMessages[activeChat] : null;
+ const [importStatus, setImportStatus] = useState<{ processed: number, total: number, status: string } | null>(null);
+
+ useEffect(() => {
+ if (!chat?.isImporting || !chat?.importJobId) {
+ setImportStatus(null);
+ return;
+ }
+
+ const poll = async () => {
+ try {
+ const data = await httpClient.request(`/import/telegram/status/${chat.importJobId}`);
+ setImportStatus({ processed: data.processedMessages, total: data.totalMessages, status: data.status });
+ if (data.status === 'Completed' || data.status === 'Failed') {
+ setImportStatus(null);
+ loadChats();
+ }
+ } catch (e: any) {
+ if (e.status === 404) {
+ console.warn('Import job not found');
+ } else {
+ console.error('Failed to poll status', e);
+ }
+ }
+ };
+
+ poll();
+ const interval = setInterval(poll, 1000);
+ return () => clearInterval(interval);
+ }, [chat?.isImporting, chat?.importJobId]);
+
// Количество непрочитанных сообщений (для бейджика)
const unreadCount = chatMessages.filter(
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
@@ -115,17 +147,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
sessionUnreadRef.current = { chatId: activeChat, msgId: firstUnreadMsg ? firstUnreadMsg.id : null };
}
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
- const lastObservedMessageIdRef = useRef(null);
const initialScrollChatId = useRef(null);
- const chatScrollPositionsRef = useRef>({});
- const visitedChatsRef = useRef>(new Set());
// Load muted state
useEffect(() => {
if (activeChat) {
setMuted(isChatMuted(activeChat));
setActiveGroupCallParticipants([]);
- lastObservedMessageIdRef.current = null;
}
}, [activeChat]);
@@ -511,32 +539,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
if (!activeChat || !chat) {
return (
- {/* Background Knot Texture - Correct Horizontal Unclosed Infinity SVG */}
+ {/* Background Knot Texture */}
-
- {/* Left part of the infinity loop */}
-
- {/* Right part of the infinity loop with a small gap at the crossing */}
-
-
+
cloud_download
{/* Chat Empty State View */}
@@ -575,8 +580,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
);
}
- const initials = getInitials(chatName || '??');
-
const handleToggleSelect = (msgId: string) => {
const newMap = new Set(selectedMessages);
if (newMap.has(msgId)) {
@@ -593,8 +596,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
setSelectedMessages(new Set([msgId]));
};
-
-
const handleForward = (targetChatId: string) => {
const socket = getSocket();
if (!socket || !activeChat) return;
@@ -655,7 +656,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
>
{selectionMode ? (
-
+
{ setSelectionMode(false); setSelectedMessages(new Set()); }} className="p-2 -ml-2 rounded-xl hover:bg-on-surface/10 transition slide-on-ice">
close
@@ -985,176 +986,218 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
)}
- {/* Закреплённое сообщение */}
- {/* Active group call banner */}
- {chat?.type === 'group' && config?.webRtc?.enabled && activeGroupCallParticipants.length > 0 && (
- onStartGroupCall?.(chat.id, chat.name || 'Group', 'voice')}
- className="flex items-center gap-3 px-4 py-2.5 border-b border-border bg-emerald-500/10 hover:bg-emerald-500/20 transition-colors text-left w-full flex-shrink-0"
- >
-
-
+ {chat.isImporting ? (
+
+
+ cloud_download
-
-
{t('activeCall')}
-
{activeGroupCallParticipants.length} {t('participants')}
-
-
{t('joinCall')}
-
- )}
- {pinnedMsg && (
-
{
- const el = document.getElementById(`msg-${pinnedMsg.id}`);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- el.classList.add('highlight-message');
- setTimeout(() => el.classList.remove('highlight-message'), 5000);
- }
- }}
- className="flex items-center gap-3 px-4 py-2 border-b border-border bg-surface-secondary/60 hover:bg-surface-hover transition-colors text-left w-full flex-shrink-0"
- >
-
-
-
{t('pinnedMessage')}
-
- {pinnedMsg.content || (pinnedMsg.media?.length > 0 ? t('media') : '...')}
+
+
+ downloading
+
+
+
+ Идет импорт истории
+
+
+ Мы переносим ваши сообщения и медиафайлы из Telegram. Это займет некоторое время.
-
- {
- e.stopPropagation();
- const socket = getSocket();
- if (socket && activeChat) {
- socket.emit('unpin_message', { messageId: pinnedMsg.id, chatId: activeChat });
- }
- }}
- />
-
- )}
- {/* Сообщения */}
-
0 ? 'invisible' : ''}`}
- >
- {isLoadingMessages && chatMessages.length === 0 ? (
-
- ) : chatMessages.length === 0 ? (
-
- ) : (
-
- {isLoadingMessages && (
-
-
+
+
+
+ {importStatus?.status === 'Processing' ? 'Обработка' :
+ importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'}
+
+
+ {importStatus?.processed || 0} / {importStatus?.total || 0}
+
+
+
+
+
+
+
+
+ Чат станет доступен автоматически
+
+
+
+
+ ) : (
+ <>
+ {chat?.type === 'group' && config?.webRtc?.enabled && activeGroupCallParticipants.length > 0 && (
+
onStartGroupCall?.(chat.id, chat.name || 'Group', 'voice')}
+ className="flex items-center gap-3 px-4 py-2.5 border-b border-outline/10 bg-emerald-500/10 hover:bg-emerald-500/20 transition-colors text-left w-full flex-shrink-0"
+ >
+
+
+
{t('activeCall')}
+
{activeGroupCallParticipants.length} {t('participants')}
+
+ {t('joinCall')}
+
+ )}
+
+ {pinnedMsg && (
+
{
+ const el = document.getElementById(`msg-${pinnedMsg.id}`);
+ if (el) {
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ el.classList.add('highlight-message');
+ setTimeout(() => el.classList.remove('highlight-message'), 5000);
+ }
+ }}
+ className="flex items-center gap-3 px-4 py-2 border-b border-outline/10 bg-surface-container-high/60 hover:bg-surface-container-highest transition-colors text-left w-full flex-shrink-0"
+ >
+
+
+
{t('pinnedMessage')}
+
+ {pinnedMsg.content || (pinnedMsg.media?.length > 0 ? t('media') : '...')}
+
+
+ {
+ e.stopPropagation();
+ const socket = getSocket();
+ if (socket && activeChat) {
+ socket.emit('unpin_message', { messageId: pinnedMsg.id, chatId: activeChat });
+ }
+ }}
+ />
+
+ )}
+
+
0 ? 'invisible' : ''}`}
+ >
+ {isLoadingMessages && chatMessages.length === 0 ? (
+
+ ) : chatMessages.length === 0 ? (
+
+ ) : (
+
+ {isLoadingMessages && (
+
+ )}
+ {chatMessages.map((msg, i) => {
+ const prevMsg = i > 0 ? chatMessages[i - 1] : null;
+ const showAvatar = !prevMsg || prevMsg.senderId !== msg.senderId;
+ const showDate =
+ !prevMsg ||
+ new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString();
+
+ const isFirstUnread = firstUnreadId === msg.id;
+
+ return (
+
r.userId === user?.id) ? 'unread-detector' : ''}`}
+ >
+ {isFirstUnread && (
+
+
+
+ {t('unreadMessages')}
+
+
+
+ )}
+ {showDate && (
+
+
+ {new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru' : 'en', {
+ day: 'numeric',
+ month: 'long',
+ })}
+
+
+ )}
+
setProfileUserId(userId)}
+ selectionMode={selectionMode}
+ isSelected={selectedMessages.has(msg.id)}
+ onToggleSelect={handleToggleSelect}
+ onStartSelectionMode={handleStartSelection}
+ onForward={(id) => {
+ setSelectedMessages(new Set([id]));
+ setShowForwardModal(true);
+ }}
+ />
+
+ );
+ })}
+
)}
- {chatMessages.map((msg, i) => {
- const prevMsg = i > 0 ? chatMessages[i - 1] : null;
- const showAvatar = !prevMsg || prevMsg.senderId !== msg.senderId;
- const showDate =
- !prevMsg ||
- new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString();
-
- const isFirstUnread = firstUnreadId === msg.id;
-
- return (
-
r.userId === user?.id) ? 'unread-detector' : ''}`}
- >
- {isFirstUnread && (
-
-
-
- {t('unreadMessages')}
-
-
-
- )}
- {showDate && (
-
-
- {new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', {
- day: 'numeric',
- month: 'long',
- })}
-
-
- )}
-
setProfileUserId(userId)}
- selectionMode={selectionMode}
- isSelected={selectedMessages.has(msg.id)}
- onToggleSelect={handleToggleSelect}
- onStartSelectionMode={handleStartSelection}
- onForward={(id) => {
- setSelectedMessages(new Set([id]));
- setShowForwardModal(true);
- }}
- />
-
- );
- })}
-
{/* Empty spacer for the bottom scroll boundary */}
- )}
-
- {/* Кнопка прокрутки вниз */}
-
- {showScrollDown && (
- {
- scrollToBottom(true);
- if (activeChat && unreadCount > 0) {
- useChatStore.getState().markAllAsRead(activeChat);
- }
- }}
- className="absolute bottom-24 right-8 w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-primary-container text-on-primary-container shadow-[0_8px_30px_rgba(48,150,229,0.3)] flex items-center justify-center transition-all z-10 border border-white/10 backdrop-blur-md"
- >
- arrow_downward
- {unreadCount > 0 && (
-
+ {showScrollDown && (
+ scrollToBottom(true)}
+ className="absolute bottom-24 right-8 w-14 h-14 rounded-2xl bg-primary text-on-primary shadow-2xl flex items-center justify-center z-20 hover:shadow-primary/20 transition-all"
>
- {unreadCount > 99 ? '99+' : unreadCount}
-
+
+ {unreadCount > 0 && (
+
+ {unreadCount > 99 ? '99+' : unreadCount}
+
+ )}
+
)}
-
- )}
-
+
+
+
+ >
+ )}
- {/* Typing индикатор */}
{typingInChat.length > 0 && (
)}
- {/* Ввод сообщения */}
- {activeChat &&
}
-
{(() => {
const handleJumpToMessage = async (msgId: string, cleanup?: () => void, targetCreatedAt?: string) => {
cleanup?.();
@@ -1173,29 +1216,23 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
if (!activeChat) return;
const NotificationStore = await import('../../../../core/application/stores/notificationStore');
- NotificationStore.useNotificationStore.getState().addNotification('info', t('searchingHistory' as any) || 'Searching message in history...');
+ NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
const chatStore = useChatStore.getState();
let found = false;
const targetDate = targetCreatedAt ? new Date(targetCreatedAt).getTime() : 0;
- // Smart timeline-based search
for (let i = 0; i < 100; i++) {
const chatMessages = chatStore.messages[activeChat] || [];
const oldestLoaded = chatMessages.length > 0 ? new Date(chatMessages[0].createdAt).getTime() : Date.now();
- // If target is newer than oldest loaded, and not found, maybe it's in a gap or we need to keep loading?
- // Actually target is almost always older if not found.
- // If we don't have targetCreatedAt, we guess (up to 100 attempts)
if (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) {
- // Should have been found by tryScroll, but lets try one last time
if (tryScroll()) { found = true; break; }
}
if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break;
await chatStore.loadMessages(activeChat, false, true);
- // Give React 150ms to render the new messages
await new Promise(resolve => setTimeout(resolve, 150));
if (tryScroll()) {
@@ -1203,21 +1240,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
break;
}
- // Stop if we have gone way past the target date
if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) {
- // We are 1 hour before the message and still haven't found it? might be deleted
if (i > 10) break;
}
}
if (!found) {
- NotificationStore.useNotificationStore.getState().addNotification('warning', t('messageNotFound' as any) || 'Message not found');
+ NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found');
}
};
return (
<>
- {/* Профиль пользователя */}
{profileUserId && (
- {/* Настройки группы */}
{showGroupSettings && chat && chat.type === 'group' && (
-
-
+
+
-
+ {(m.url?.toLowerCase().endsWith('.gif') || m.filename?.toLowerCase().endsWith('.gif')) ? (
+
+ ) : (
+
+ )}
{ e.stopPropagation(); onGoToMessage?.(m.messageId); }}
className="absolute bottom-2 right-2 px-2 py-1 rounded-md bg-black/60 backdrop-blur-sm flex items-center justify-center text-white text-[10px] font-medium opacity-0 group-hover:opacity-100 transition-opacity hover:bg-black/80 shadow-md"
diff --git a/client-web/src/modules/chats/presentation/components/MessageBubble.tsx b/client-web/src/modules/chats/presentation/components/MessageBubble.tsx
index 1387158..fe6f4e0 100644
--- a/client-web/src/modules/chats/presentation/components/MessageBubble.tsx
+++ b/client-web/src/modules/chats/presentation/components/MessageBubble.tsx
@@ -347,7 +347,7 @@ function MessageBubble({
const isMediaGif = (m: MediaItem) => {
if (m.type === 'gif') return true;
if (m.url?.toLowerCase().includes('klipy') || m.url?.toLowerCase().endsWith('.gif')) return true;
- if (m.filename?.toLowerCase().includes('gif')) return true;
+ if (m.filename?.toLowerCase().includes('gif') || m.filename?.toLowerCase().endsWith('.mp4') || m.filename?.toLowerCase().endsWith('.gif')) return true;
if (m.type === 'image' && m.url?.toLowerCase().endsWith('.mp4')) return true;
return false;
};
@@ -643,12 +643,20 @@ function MessageBubble({
className={`relative cursor-pointer group/video overflow-hidden transition-all hover:brightness-90 bg-zinc-900 ${cellClass}`}
onClick={() => setLightboxData({ index: idx })}
>
- {gif && m.url?.toLowerCase().endsWith('.mp4') ? (
-
+ {gif ? (
+ (m.url?.toLowerCase().endsWith('.gif') || m.filename?.toLowerCase().endsWith('.gif')) ? (
+
+ ) : (
+
+ )
) : m.type === 'video' ? (
<>
{m.thumbnail ? (
diff --git a/client-web/src/modules/chats/presentation/components/NewChatModal.tsx b/client-web/src/modules/chats/presentation/components/NewChatModal.tsx
index 92f6952..a602264 100644
--- a/client-web/src/modules/chats/presentation/components/NewChatModal.tsx
+++ b/client-web/src/modules/chats/presentation/components/NewChatModal.tsx
@@ -13,11 +13,12 @@ import { getInitials } from '../../../../core/utils/utils';
interface NewChatModalProps {
onClose: () => void;
+ onOpenTelegramImport?: () => void;
}
type Mode = 'personal' | 'group-select' | 'group-name';
-export default function NewChatModal({ onClose }: NewChatModalProps) {
+export default function NewChatModal({ onClose, onOpenTelegramImport }: NewChatModalProps) {
const { user, config } = useAuthStore();
const { t } = useLang();
const { addChat, setActiveChat, loadMessages } = useChatStore();
@@ -225,7 +226,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
onClick={() => setMode('group-select')}
className="w-full flex items-center gap-4 px-4 py-3.5 rounded-2xl bg-surface-container-highest/20 hover:bg-surface-container-highest/40 transition-all border border-white/5 active:scale-[0.98] group"
>
-
+
@@ -237,6 +238,26 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
)}
+ {config?.import?.enableTelegramImport && mode === 'personal' && (
+
{
+ onOpenTelegramImport?.();
+ onClose();
+ }}
+ className="w-full flex items-center gap-4 px-4 py-3.5 rounded-2xl bg-surface-container-highest/20 hover:bg-surface-container-highest/40 transition-all border border-white/5 active:scale-[0.98] group"
+ >
+
+
+
+
+
{t('importTelegram')}
+
+ {t('importTelegramDesc')}
+
+
+
+ )}
+
{/* Выбранные (в режиме группы) */}
{mode === 'group-select' && selectedUsers.length > 0 && (
diff --git a/client-web/src/modules/users/presentation/components/TelegramImportModal.tsx b/client-web/src/modules/users/presentation/components/TelegramImportModal.tsx
index ef98546..3a1b3f4 100644
--- a/client-web/src/modules/users/presentation/components/TelegramImportModal.tsx
+++ b/client-web/src/modules/users/presentation/components/TelegramImportModal.tsx
@@ -5,6 +5,7 @@ import { AppApi } from '../../../../core/infrastructure/appApi';
import { useLang } from '../../../../core/infrastructure/i18n';
import type { User as UserType, FriendWithId } from '../../../../core/domain/types';
import { useAuthStore } from '../../../auth/application/authStore';
+import { useChatStore } from '../../../chats/application/chatStore';
interface TelegramImportModalProps {
isOpen: boolean;
@@ -54,7 +55,7 @@ export default function TelegramImportModal({ isOpen, onClose, friends }: Telegr
setStep(2);
} catch (err: any) {
console.error(err);
- setError(err.message || 'Ошибка загрузки файла');
+ setError(err.message || 'Ошибка анализа файла');
} finally {
setLoading(false);
}
@@ -67,12 +68,17 @@ export default function TelegramImportModal({ isOpen, onClose, friends }: Telegr
try {
const res = await AppApi.executeTelegramImport({ token, mapping, groupName }) as any;
- setImportedState({ count: res.messagesImported, text: 'Успешно импортировано' });
+ setImportedState({ count: 0, text: 'Импорт запущен в фоновом режиме' });
setStep(3);
+
+ // Обновляем список чатов и выбираем новый
+ await useChatStore.getState().loadChats();
+ if (res.chatId) {
+ useChatStore.getState().setActiveChat(res.chatId);
+ }
} catch (err: any) {
console.error(err);
setError(err.message || 'Ошибка импорта');
- setStep(1);
} finally {
setLoading(false);
}
@@ -103,47 +109,56 @@ export default function TelegramImportModal({ isOpen, onClose, friends }: Telegr
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- className="absolute inset-0 bg-black/60 backdrop-blur-sm"
+ className="absolute inset-0 bg-black/40 backdrop-blur-md"
onClick={step === 3 && importedState ? handleClose : undefined}
/>
{/* Header */}
-
-
-
-
Импорт из Telegram
+
+
+
+
+
+
{t('importTelegram')}
-
+
{error && (
-
-
-
{error}
+
+
+
+
{t('error')}
+
{error}
+
)}
{step === 1 && (
-
-
-
+
+
fileInputRef.current?.click()}
+ className="w-24 h-24 mx-auto bg-surface-container-highest/20 rounded-[2rem] flex items-center justify-center border border-white/5 shadow-2xl cursor-pointer hover:bg-surface-container-highest/30 active:scale-95 transition-all group"
+ >
+
-
-
Загрузите архив с историей
-
- Скачайте историю чата из Telegram в формате HTML (сняв галочку с формата JSON). Убедитесь, что медиафайлы тоже скачаны, если хотите перенести их. Загрузите полученный ZIP архив.
-
+
+
{t('importSelectArchive')}
+
+ {t('importSelectArchiveDesc')}