Compare commits

..

13 Commits

Author SHA1 Message Date
Tim
3a742fbb00 fix: tabs ui格式统一 #710 2025-08-25 13:56:42 +08:00
Tim
743c3dbc72 Merge pull request #715 from nagisa77/codex/update-reactionemojimap-to-google-emoji-cdn
feat: use Google emoji CDN
2025-08-25 11:03:21 +08:00
Tim
d46a446f2b feat: use Google emoji CDN 2025-08-25 11:03:06 +08:00
Tim
75a785f612 Merge branch 'feature/daily_bugfix_0825' of github.com:nagisa77/OpenIsle into feature/daily_bugfix_0825 2025-08-25 11:02:22 +08:00
Tim
e79b75f340 fix: tabs ui格式统一 #710 2025-08-25 11:01:52 +08:00
Tim
1f6f470ab5 Merge pull request #713 from nagisa77/codex/limit-base-timeline-hover-to-messages
feat: limit BaseTimeline hover to private messages
2025-08-25 10:29:52 +08:00
Tim
583d4042f5 feat: add optional hover for BaseTimeline 2025-08-25 10:29:35 +08:00
Tim
8437c1c714 Merge pull request #709 from nagisa77/codex/add-globalpopup-for-internal-messages
feat: add message feature popup
2025-08-23 02:58:34 +08:00
Tim
2613fe6cf1 feat: introduce message popup component 2025-08-23 02:58:24 +08:00
Tim
a15d541b72 Merge pull request #699 from nagisa77/feature/daily_bugfix_0823
daily bugfix
2025-08-23 02:49:36 +08:00
Tim
8657a06f52 Merge pull request #708 from nagisa77/codex/restrict-conversation-search-to-direct-messages
Fix findOrCreateConversation to only retrieve private conversations
2025-08-23 02:46:53 +08:00
Tim
09900b34aa Restrict conversation lookup to private chats 2025-08-23 02:46:41 +08:00
Tim
4e1c3f5839 Merge pull request #707 from nagisa77/codex/fix-multiple-results-error-in-findconversationbyusers
Handle multiple conversations between users
2025-08-23 02:39:28 +08:00
10 changed files with 210 additions and 57 deletions

View File

@@ -6,7 +6,9 @@ package com.openisle.model;
public enum ReactionType {
LIKE,
DISLIKE,
SMILE,
RECOMMEND,
CONGRATULATIONS,
ANGRY,
FLUSHED,
STAR_STRUCK,
@@ -26,5 +28,5 @@ public enum ReactionType {
CHINA,
USA,
JAPAN,
KOREA
KOREA,
}

View File

@@ -11,8 +11,11 @@ import java.util.List;
@Repository
public interface MessageConversationRepository extends JpaRepository<MessageConversation, Long> {
@Query("SELECT c FROM MessageConversation c JOIN c.participants p1 JOIN c.participants p2 " +
"WHERE p1.user = :user1 AND p2.user = :user2 ORDER BY c.createdAt DESC")
@Query("SELECT c FROM MessageConversation c " +
"WHERE c.channel = false AND size(c.participants) = 2 " +
"AND EXISTS (SELECT 1 FROM c.participants p1 WHERE p1.user = :user1) " +
"AND EXISTS (SELECT 1 FROM c.participants p2 WHERE p2.user = :user2) " +
"ORDER BY c.createdAt DESC")
List<MessageConversation> findConversationsByUsers(@Param("user1") User user1, @Param("user2") User user2);
@Query("SELECT DISTINCT c FROM MessageConversation c " +

View File

@@ -1,5 +1,5 @@
<template>
<div class="timeline">
<div class="timeline" :class="{ 'hover-enabled': hover }">
<div class="timeline-item" v-for="(item, idx) in items" :key="idx">
<div
class="timeline-icon"
@@ -8,7 +8,7 @@
>
<img v-if="item.src" :src="item.src" class="timeline-img" alt="timeline item" />
<i v-else-if="item.icon" :class="item.icon"></i>
<span v-else-if="item.emoji" class="timeline-emoji">{{ item.emoji }}</span>
<img v-else-if="item.emoji" :src="item.emoji" class="timeline-emoji" alt="emoji" />
</div>
<div class="timeline-content">
<slot name="item" :item="item">{{ item.content }}</slot>
@@ -22,6 +22,7 @@ export default {
name: 'BaseTimeline',
props: {
items: { type: Array, default: () => [] },
hover: { type: Boolean, default: false },
},
}
</script>
@@ -41,7 +42,7 @@ export default {
margin-top: 10px;
}
.timeline-item:hover {
.hover-enabled .timeline-item:hover {
background-color: var(--menu-selected-background-color);
transition: background-color 0.2s;
border-radius: 10px;
@@ -73,8 +74,9 @@ export default {
}
.timeline-emoji {
font-size: 20px;
line-height: 1;
width: 20px;
height: 20px;
object-fit: contain;
}
.timeline-item::before {

View File

@@ -7,6 +7,7 @@
@close="closeMilkTeaPopup"
/>
<NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" />
<MessagePopup :visible="showMessagePopup" @close="closeMessagePopup" />
<MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" />
<ActivityPopup
@@ -22,6 +23,7 @@
import ActivityPopup from '~/components/ActivityPopup.vue'
import MedalPopup from '~/components/MedalPopup.vue'
import NotificationSettingPopup from '~/components/NotificationSettingPopup.vue'
import MessagePopup from '~/components/MessagePopup.vue'
import { authState } from '~/utils/auth'
const config = useRuntimeConfig()
@@ -33,6 +35,7 @@ const milkTeaIcon = ref('')
const inviteCodeIcon = ref('')
const showNotificationPopup = ref(false)
const showMessagePopup = ref(false)
const showMedalPopup = ref(false)
const newMedals = ref([])
@@ -43,6 +46,9 @@ onMounted(async () => {
await checkInviteCodeActivity()
if (showInviteCodePopup.value) return
await checkMessageFeature()
if (showMessagePopup.value) return
await checkNotificationSetting()
if (showNotificationPopup.value) return
@@ -97,6 +103,18 @@ const closeMilkTeaPopup = () => {
showMilkTeaPopup.value = false
}
const checkMessageFeature = async () => {
if (!import.meta.client) return
if (!authState.loggedIn) return
if (localStorage.getItem('messageFeaturePopupShown')) return
showMessagePopup.value = true
}
const closeMessagePopup = () => {
if (!import.meta.client) return
localStorage.setItem('messageFeaturePopupShown', 'true')
showMessagePopup.value = false
}
const checkNotificationSetting = async () => {
if (!import.meta.client) return
if (!authState.loggedIn) return

View File

@@ -0,0 +1,74 @@
<template>
<BasePopup :visible="visible" @close="close">
<div class="message-popup">
<div class="message-popup-title">📨 站内信上线啦</div>
<div class="message-popup-text">现在可以在右上角使用站内信功能</div>
<div class="message-popup-actions">
<div class="message-popup-close" @click="close">知道了</div>
<div class="message-popup-button" @click="gotoMessage">去看看</div>
</div>
</div>
</BasePopup>
</template>
<script setup>
import BasePopup from '~/components/BasePopup.vue'
defineProps({
visible: { type: Boolean, default: false },
})
const emit = defineEmits(['close'])
const gotoMessage = () => {
emit('close')
navigateTo('/message-box', { replace: true })
}
const close = () => emit('close')
</script>
<style scoped>
.message-popup {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
min-width: 200px;
}
.message-popup-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.message-popup-actions {
margin-top: 10px;
display: flex;
flex-direction: row;
gap: 20px;
}
.message-popup-button {
background-color: var(--primary-color);
color: #fff;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
}
.message-popup-button:hover {
background-color: var(--primary-color-hover);
}
.message-popup-close {
cursor: pointer;
color: var(--primary-color);
display: flex;
align-items: center;
}
.message-popup-close:hover {
text-decoration: underline;
}
</style>

View File

@@ -3,20 +3,37 @@
<div class="reactions-viewer">
<div
class="reactions-viewer-item-container"
@click="openPanel"
@mouseenter="cancelHide"
@mouseleave="scheduleHide"
>
<template v-if="displayedReactions.length">
<div v-for="r in displayedReactions" :key="r.type" class="reactions-viewer-item">
{{ reactionEmojiMap[r.type] }}
<template v-if="reactions.length < 4">
<div
v-for="r in displayedReactions"
:key="r.type"
class="reactions-viewer-single-item"
:class="{ selected: userReacted(r.type) }"
@click="toggleReaction(r.type)"
>
<img :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
<div>{{ counts[r.type] }}</div>
</div>
<div class="reactions-viewer-item placeholder" @click="openPanel">
<i class="far fa-smile"></i>
<!-- <span class="reactions-viewer-item-placeholder-text">点击以表态</span> -->
</div>
</template>
<template v-else-if="displayedReactions.length">
<div
v-for="r in displayedReactions"
:key="r.type"
class="reactions-viewer-item"
@click="openPanel"
>
<img :src="reactionEmojiMap[r.type]" class="emoji" alt="emoji" />
</div>
<div class="reactions-count">{{ totalCount }}</div>
</template>
<div v-else class="reactions-viewer-item placeholder">
<i class="far fa-smile"></i>
<span class="reactions-viewer-item-placeholder-text">点击以表态</span>
</div>
</div>
</div>
<div class="make-reaction-container">
@@ -40,7 +57,9 @@
@click="toggleReaction(t)"
:class="{ selected: userReacted(t) }"
>
{{ reactionEmojiMap[t] }}<span v-if="counts[t]">{{ counts[t] }}</span>
<img :src="reactionEmojiMap[t]" class="emoji" alt="emoji" /><span v-if="counts[t]">{{
counts[t]
}}</span>
</div>
</div>
</div>
@@ -217,13 +236,6 @@ onMounted(async () => {
font-size: 16px;
}
.reactions-viewer-item.placeholder {
opacity: 0.5;
display: flex;
flex-direction: row;
align-items: center;
}
.reactions-viewer-item-placeholder-text {
font-size: 14px;
padding-left: 5px;
@@ -262,18 +274,16 @@ onMounted(async () => {
.reactions-panel {
position: absolute;
bottom: 40px;
left: -20px;
bottom: 50px;
background-color: var(--background-color);
border: 1px solid var(--normal-border-color);
border-radius: 5px;
padding: 5px;
max-width: 240px;
border-radius: 20px;
padding: 5px 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
z-index: 10;
gap: 2px;
gap: 5px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
}
@@ -287,6 +297,27 @@ onMounted(async () => {
gap: 2px;
}
.reactions-viewer-item.placeholder,
.reactions-viewer-single-item {
display: flex;
cursor: pointer;
flex-direction: row;
padding: 2px 10px;
gap: 5px;
border: 1px solid var(--normal-border-color);
border-radius: 10px;
margin-right: 5px;
margin-bottom: 5px;
font-size: 14px;
color: var(--text-color);
align-items: center;
}
.reactions-viewer-item.placeholder,
.reactions-viewer-single-item.selected {
background-color: var(--menu-selected-background-color);
}
.reaction-option.selected {
background-color: var(--menu-selected-background-color);
}

View File

@@ -652,6 +652,10 @@ const sanitizeDescription = (text) => stripMarkdown(text)
}
@container home-page (max-width: 768px) {
.topic-item-container {
margin-left: 0px;
gap: 0px;
}
.article-main-container,
.header-item.main-item {
width: calc(70% - 20px);
@@ -709,6 +713,16 @@ const sanitizeDescription = (text) => stripMarkdown(text)
.topic-container {
position: initial;
padding: 0;
}
.topic-item {
padding: 10px 20px;
}
.topic-select-container {
margin-left: 10px;
margin-top: 10px;
}
}
</style>

View File

@@ -20,7 +20,7 @@
{{ loadingMore ? '加载中...' : '查看更多消息' }}
</div>
</div>
<BaseTimeline :items="messages">
<BaseTimeline :items="messages" hover>
<template #item="{ item }">
<div class="message-header">
<div class="user-name">

View File

@@ -287,13 +287,13 @@ function goToConversation(id) {
}
.tab {
padding: 8px 16px;
padding: 10px 20px;
cursor: pointer;
}
.tab.active {
font-weight: 600;
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
}
.loading-message {

View File

@@ -1,25 +1,34 @@
export const reactionEmojiMap = {
LIKE: '❤️',
DISLIKE: '👎',
RECOMMEND: '👏',
ANGRY: '😡',
FLUSHED: '😳',
STAR_STRUCK: '🤩',
ROFL: '🤣',
HOLDING_BACK_TEARS: '🥹',
MIND_BLOWN: '🤯',
POOP: '💩',
CLOWN: '🤡',
SKULL: '☠️',
FIRE: '🔥',
EYES: '👀',
FROWN: '☹️',
HOT: '🥵',
EAGLE: '🦅',
SPIDER: '🕷️',
BAT: '🦇',
CHINA: '🇨🇳',
USA: '🇺🇸',
JAPAN: '🇯🇵',
KOREA: '🇰🇷',
const toCdnUrl = (emoji) => {
const codepoints = Array.from(emoji)
.map((c) => c.codePointAt(0).toString(16))
.join('_')
return `https://fonts.gstatic.com/s/e/notoemoji/latest/${codepoints}/emoji.svg`
}
export const reactionEmojiMap = {
LIKE: toCdnUrl('❤️'),
SMILE: toCdnUrl('😁'),
DISLIKE: toCdnUrl('👎'),
RECOMMEND: toCdnUrl('👏'),
CONGRATULATIONS: toCdnUrl('🎉'),
ANGRY: toCdnUrl('😡'),
FLUSHED: toCdnUrl('😳'),
STAR_STRUCK: toCdnUrl('🤩'),
ROFL: toCdnUrl('🤣'),
HOLDING_BACK_TEARS: toCdnUrl('🥹'),
MIND_BLOWN: toCdnUrl('🤯'),
POOP: toCdnUrl('💩'),
CLOWN: toCdnUrl('🤡'),
SKULL: toCdnUrl('☠️'),
FIRE: toCdnUrl('🔥'),
EYES: toCdnUrl('👀'),
FROWN: toCdnUrl('☹️'),
HOT: toCdnUrl('🥵'),
EAGLE: toCdnUrl('🦅'),
SPIDER: toCdnUrl('🕷️'),
BAT: toCdnUrl('🦇'),
CHINA: toCdnUrl('🇨🇳'),
USA: toCdnUrl('🇺🇸'),
JAPAN: toCdnUrl('🇯🇵'),
KOREA: toCdnUrl('🇰🇷'),
}