···1818// along with this program. If not, see <https://www.gnu.org/licenses/>.
19192020import gleam/dynamic/decode
2121-import lumina_client/message_type
2121+import lumina_client/model_type
22222323/// Get the color scheme of the user's system (media query)
2424@external(javascript, "./dom_ffi.mjs", "get_color_scheme")
···3434pub fn start_dragging_modal_box(
3535 curr_x: Float,
3636 curr_y: Float,
3737- constructor: fn(Float, Float) -> message_type.Msg,
3838- dispatch: fn(message_type.Msg) -> Nil,
3737+ constructor: fn(Float, Float) -> model_type.Msg,
3838+ dispatch: fn(model_type.Msg) -> Nil,
3939) -> Nil
40404141/// Get the window dimensions in pixels
4242/// Returns: #(width_px, height_px)
4343-///
4343+///
4444/// // This should be used in an effect and saved to the model, not called directly in views, but is for now called as an helper in views.
4545@external(javascript, "./dom_ffi.mjs", "get_window_dimensions_px")
4646pub fn get_window_dimensions_px() -> #(Int, Int)
+1-4
client/src/lumina_client/helpers.gleam
···1717// You should have received a copy of the GNU Affero General Public License
1818// along with this program. If not, see <https://www.gnu.org/licenses/>.
19192020-import gleam/float
2120import gleam/int
2221import gleam/list
2323-import gleam/result
2422import lumina_client/dom
2525-import lumina_client/message_type.{type Msg}
2626-import lumina_client/model_type.{type LoginFields}
2323+import lumina_client/model_type.{type LoginFields, type Msg}
2724import lustre/attribute
2825import plinth/javascript/global
2926
-54
client/src/lumina_client/message_type.gleam
···11-// Lumina/Peonies
22-// Copyright (C) 2018-2026 MLC 'Strawmelonjuice' Bloeiman and contributors.
33-//
44-// This program is free software: you can redistribute it and/or modify
55-// it under the terms of the GNU Affero General Public License as published
66-// by the Free Software Foundation, either version 3 of the License, or
77-// (at your option) any later version.
88-//
99-// This program is distributed in the hope that it will be useful,
1010-// but WITHOUT ANY WARRANTY; without even the implied warranty of
1111-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1212-// GNU Affero General Public License for more details.
1313-//
1414-// You should have received a copy of the GNU Affero General Public License
1515-// along with this program. If not, see <https://www.gnu.org/licenses/>.
1616-1717-import lustre_websocket
1818-1919-pub type Msg {
2020- WSTryReconnect
2121- Past150ms
2222- UpdateLastRefreshRequestTime(Int)
2323- WsDisconnectDefinitive
2424- WsWrapper(lustre_websocket.WebSocketEvent)
2525- ToLoginPage
2626- SubmitLogin(List(#(String, String)))
2727- ToRegisterPage
2828- SubmitSignup(List(#(String, String)))
2929- ToLandingPage
3030- // Can be re-used for both login and register pages
3131- UpdateEmailField(String)
3232- UpdatePasswordField(String)
3333- // Register page
3434- UpdateUsernameField(String)
3535- UpdatePasswordConfirmField(String)
3636- FocusLostEmailField
3737- /// Travel to a different timeline.
3838- TimeLineTo(String)
3939- /// Load more posts for the current timeline
4040- LoadMorePosts(String)
4141- /// Log the user out (destroys session and recreates model)
4242- Logout
4343- /// Close current modal
4444- CloseModal
4545- /// Browse modal to different page
4646- SetModal(String)
4747- /// Start dragging the modal box
4848- /// Parameters: the event, current mouse x and y positions
4949- /// Starts a sideffect that tracks mouse movements and sends MoveModalBoxTo messages
5050- StartDraggingModalBox(Float, Float)
5151- /// Move the modal box to a new position
5252- /// Parameters: new x and y positions
5353- MoveModalBoxTo(Float, Float)
5454-}
+80-15
client/src/lumina_client/model_type.gleam
···2222import gleam/json
2323import gleam/list
2424import gleam/option.{type Option, None, Some}
2525+import gleam/uri.{type Uri}
2526import lustre_websocket
26272828+pub type Msg {
2929+ WSTryReconnect
3030+ Past150ms
3131+ UpdateLastRefreshRequestTime(Int)
3232+ WsDisconnectDefinitive
3333+ WsWrapper(lustre_websocket.WebSocketEvent)
3434+ ToLoginPage
3535+ SubmitLogin(List(#(String, String)))
3636+ ToRegisterPage
3737+ SubmitSignup(List(#(String, String)))
3838+ ToLandingPage
3939+ // Can be re-used for both login and register pages
4040+ UpdateEmailField(String)
4141+ UpdatePasswordField(String)
4242+ // Register page
4343+ UpdateUsernameField(String)
4444+ UpdatePasswordConfirmField(String)
4545+ FocusLostEmailField
4646+ /// Travel to a different timeline.
4747+ TimeLineTo(String)
4848+ /// Load more posts for the current timeline
4949+ LoadMorePosts(String)
5050+ /// Log the user out (destroys session and recreates model)
5151+ Logout
5252+ /// Close current modal
5353+ CloseModal
5454+ /// Browse modal to different page
5555+ SetModal(String)
5656+ /// Start dragging the modal box
5757+ /// Parameters: the event, current mouse x and y positions
5858+ /// Starts a sideffect that tracks mouse movements and sends MoveModalBoxTo messages
5959+ StartDraggingModalBox(Float, Float)
6060+ /// Move the modal box to a new position
6161+ /// Parameters: new x and y positions
6262+ MoveModalBoxTo(Float, Float)
6363+}
6464+6565+pub type Route =
6666+ Page
6767+6868+pub fn parse_route(uri: Uri) -> Route {
6969+ case uri.path_segments(uri.path) {
7070+ [] | [""] -> Landing
7171+ ["login"] -> Login(fields: LoginFields("", ""), success: None)
7272+ ["signup"] ->
7373+ Register(fields: RegisterPageFields("", "", "", ""), ready: None)
7474+ ["publication", _post_id] -> {
7575+ todo as "We don't have a publication zoom Page variant yet."
7676+ }
7777+ ["home"] | ["timeline"] -> HomeTimeline(None, None)
7878+ ["timeline", tid] -> HomeTimeline(Some(tid), None)
7979+ ["licence"] | ["license"] -> Licence
8080+8181+ _ -> NotFound(uri:)
8282+ }
8383+}
8484+8585+/// # Page
8686+///
8787+/// Lumina has always been an SPA behind the login page, splitting the three "main" pages: Login, Signup, and Home from "subpages". Home contained subpages like Dashboard, Profile, and Settings, etc.
8888+/// In this model, Login and Dashboard would be equal. The model keeps track of the current page and the user's authentication status.
8989+/// The Page type is, pretty explanatory, an enum of all the pages in the app. Nested if needed, to track fields like the current tab in the Dashboard or the username form field in the login page.
9090+pub type Page {
9191+ Landing
9292+ Register(fields: RegisterPageFields, ready: Option(Result(Nil, String)))
9393+ Login(fields: LoginFields, success: Option(Bool))
9494+ HomeTimeline(
9595+ timeline_name: Option(String),
9696+ modal: Option(#(String, Dict(String, String))),
9797+ )
9898+ Licence
9999+ NotFound(uri: Uri)
100100+}
101101+27102/// # Model
28103///
29104pub type Model {
30105 Model(
31106 /// Page currently browsing.
107107+ /// This is synced to the url through modem, but can contain more context.
32108 page: Page,
33109 /// User, if known
34110 user: Option(UserSubmodel),
···197273 )
198274}
199275200200-/// # Page
201201-///
202202-/// Lumina has always been an SPA behind the login page, splitting the three "main" pages: Login, Signup, and Home from "subpages". Home contained subpages like Dashboard, Profile, and Settings, etc.
203203-/// In this model, Login and Dashboard would be equal. The model keeps track of the current page and the user's authentication status.
204204-/// The Page type is, pretty explanatory, an enum of all the pages in the app. Nested if needed, to track fields like the current tab in the Dashboard or the username form field in the login page.
205205-pub type Page {
206206- Landing
207207- Register(fields: RegisterPageFields, ready: Option(Result(Nil, String)))
208208- Login(fields: LoginFields, success: Option(Bool))
209209- HomeTimeline(
210210- timeline_name: Option(String),
211211- modal: Option(#(String, Dict(String, String))),
212212- )
213213-}
214214-215276fn encode_page(page: Page) -> json.Json {
216277 case page {
217278 Landing -> json.object([#("type", json.string("landing"))])
···260321 Some(i) -> [#("modal", json.string(i.0))]
261322 }),
262323 )
324324+ NotFound(_) -> json.object([#("type", json.string("landing"))])
325325+326326+ Licence -> json.object([#("type", json.string("licence"))])
263327 }
264328}
265329···267331 use variant <- decode.field("type", decode.string)
268332 case variant {
269333 "landing" -> decode.success(Landing)
334334+ "licence" -> decode.success(Licence)
270335 "register" -> {
271336 use fields <- decode.field("fields", {
272337 use usernamefield <- decode.field("usernamefield", decode.string)
+14-12
client/src/lumina_client/view.gleam
···2525import lumina_client/helpers.{
2626 get_color_scheme, login_view_checker, model_local_storage_key,
2727}
2828-import lumina_client/message_type.{
2929- type Msg, SubmitLogin, SubmitSignup, ToLandingPage, ToLoginPage,
2828+import lumina_client/model_type.{
2929+ type Model, type Msg, HomeTimeline, Landing, Licence, Login, NotFound,
3030+ Register, SubmitLogin, SubmitSignup, ToLandingPage, ToLoginPage,
3031 ToRegisterPage, UpdateEmailField, UpdatePasswordConfirmField,
3132 UpdatePasswordField, UpdateUsernameField, WSTryReconnect,
3232-}
3333-import lumina_client/model_type.{
3434- type Model, HomeTimeline, Landing, Login, Register,
3533}
3634import lumina_client/view/common_view_parts.{common_view_parts}
3735import lumina_client/view/common_view_parts/svgs
···5149 model_local_storage_key,
5250 model_type.serialize(model),
5351 )
5252+ let content = case model.page {
5353+ Landing -> view_landing()
5454+ Register(..) -> view_register(model)
5555+ Login(..) -> view_login(model)
5656+ HomeTimeline(..) -> view_homepage(model)
5757+ NotFound(uri:) -> todo as "No 404 page yet."
5858+ Licence ->
5959+ todo as "Licence should be shown by the client if it's not shown by the server."
6060+ }
5461 html.div(
5562 [get_color_scheme(model), attribute.class("w-screen h-screen content")],
5663 [
···115122 model_type.WsConnectionConnected(..) | model_type.WsConnectionUnsure ->
116123 element.none()
117124 },
118118- case model.page {
119119- Landing -> view_landing()
120120- Register(..) -> view_register(model)
121121- Login(..) -> view_login(model)
122122- HomeTimeline(..) -> view_homepage(model)
123123- },
125125+ content,
124126 ],
125127 )
126128}
···518520 attribute.value(fieldvalues.emailfield),
519521 event.on_input(UpdateEmailField),
520522 event.on("focusout", {
521521- decode.success(message_type.FocusLostEmailField)
523523+ decode.success(model_type.FocusLostEmailField)
522524 }),
523525 ]),
524526 html.label([attribute.class("fieldset-label")], [
···1818// along with this program. If not, see <https://www.gnu.org/licenses/>.
19192020import gleam/list
2121-import lumina_client/message_type
2121+import lumina_client/model_type
2222import lustre/attribute.{attribute, class}
2323import lustre/element
2424import lustre/element/svg
···35353636/// Lists the SVG functions in a random order with their source URLs.
3737pub fn sources_solar_linear() -> List(
3838- #(fn(String) -> element.Element(message_type.Msg), String),
3838+ #(fn(String) -> element.Element(model_type.Msg), String),
3939) {
4040 sourcelist_solar_linear |> list.shuffle()
4141}
···2222aware.
23232424Which is why we also would need to log every timeline request to be able to identify for example over-requested
2525-timelines.2525+timelines.