The hardest message to send is "how are you?" — because the honest answer takes effort, and the dishonest answer ("fine") feels worse than not asking. We wanted a feature that let people share a vibe without the conversation overhead of explaining it.
That's mood pulse. Each morning Knotos asks one question: how do you feel today? Five soft choices. Your avatar gets a colored halo for the next 24 hours. Your circle sees the color, knows the vibe, and approaches the conversation differently — or doesn't start one at all.
The five moods
- 🤍 Calm — sage green. Quiet, content, present.
- 🧡 Warm — soft peach. Open, friendly, social.
- 💖 Love — terracotta. Affectionate, grateful, full.
- 💙 Low — muted blue. Heavy, quiet, please be gentle.
- ✨ Spark — accent gold. Energetic, creative, ready.
We agonized over the count. Three felt too coarse. Seven felt like a personality test. Five gave us one-row + one-row layout (3 + 2) on small screens, distinct enough colors to remember, and a small enough vocabulary that most days you can pick one in under a second.
Why "low" instead of "sad"
The original vocabulary had "sad," "stressed," "anxious." All accurate. All medical. We replaced them with one word: low. It's ambiguous on purpose — it could mean tired, blue, mourning, or just rainy-day flat. The other person doesn't need to know what kind of low. They just need to know to be gentle.
The 24-hour fade
Mood updates expire after 24 hours. The avatar halo fades automatically. We considered making it permanent (it's their last set mood, why not?) and we considered making it shorter (4-8 hours for "today only"). 24 hours won because:
- Long enough that one prompt covers a whole day, including evening.
- Short enough that stale mood doesn't mislead — "low" from yesterday morning isn't accurate now.
- Aligns with the daily-prompt cadence, so users never have to think about clearing it.
Implementation
// User schema (NestJS / Mongoose)
@Prop({ enum: ['calm', 'warm', 'love', 'low', 'spark'] })
mood?: 'calm' | 'warm' | 'love' | 'low' | 'spark';
@Prop()
moodUpdatedAt?: Date;
// Client-side fade (iOS, Swift)
var isMoodActive: Bool {
guard let updatedAt = moodUpdatedAt else { return false }
return Date().timeIntervalSince(updatedAt) < 24 * 60 * 60
}Backend just stores the value and timestamp. The fade-out is purely client-side. We considered sending a server-side TTL but kept it simple: every read includes moodUpdatedAt, the client decides whether to render it. Less coupling, less to break.
The avatar ring
A flat colored dot would have worked. We didn't want flat. We wanted the mood to feelalive, like a heartbeat. So the ring uses a soft gradient stroke (plum → peach) tinted by the mood color, with a blurred halo behind the avatar at low opacity. It breathes — pulses every ~2.6 seconds with a gentle scale animation.
The animation runs at 20fps via SwiftUI's .animation(...repeatForever), paused on .onDisappear. Tested on iPhone 12 mini through 16 Pro Max with no perceptible energy impact in 8-hour Time Profiler runs.
The daily prompt
The prompt fires once per day, anytime the user opens the app after 5am. We chose 5am because before that, most users are asleep — we don't want to be the first thing they see at 3am insomnia. We chose "once per day" because anything more frequent feels like a chore.
If the user already set a mood within the last 24 hours, no prompt. If they tap "Để sau" (later), we mark them as prompted for the day. We don't re-prompt later. The whole point is gentle.
Privacy
One toggle in Settings → Privacy: Share mood with my circle. Off → mood is local-only, your contacts see no halo. We default to on (with contacts only — never "everyone") because the feature is most useful when it's reciprocal, but we wanted opt-out to be one tap.
Mood pulse is the smallest feature in Knotos and the one I think about most. It's an honest attempt to compress "how are you?" into something low-effort enough that people actually do it.