diff --git a/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
new file mode 100644
index 0000000..8d608e3
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/BaseTimelineItem.razor
@@ -0,0 +1,33 @@
+@using LibMatrix
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Homeservers
+@using LibMatrix.Responses
+<h3>BaseTimelineItem</h3>
+
+@code {
+
+ [Parameter]
+ public StateEventResponse Event { get; set; }
+
+ [Parameter]
+ public List<StateEventResponse> Events { get; set; }
+
+ [Parameter]
+ public AuthenticatedHomeserverGeneric Homeserver { get; set; }
+
+ public List<StateEventResponse> EventsBefore => Events.TakeWhile(e => e.EventId != Event.EventId).ToList();
+
+ public List<StateEventResponse> MatchingEventsBefore => EventsBefore.Where(x => x.Type == Event.Type && x.StateKey == Event.StateKey).ToList();
+
+ public StateEventResponse? PreviousState => MatchingEventsBefore.LastOrDefault();
+
+ public RoomMemberEventContent? CurrentSenderMemberEventContent => EventsBefore.LastOrDefault(x => x.Type == "m.room.member" && x.StateKey == Event.Sender)?
+ .TypedContent as RoomMemberEventContent;
+
+ public UserProfileResponse CurrentSenderProfile => new() { DisplayName = CurrentSenderMemberEventContent?.DisplayName, AvatarUrl = CurrentSenderMemberEventContent?.AvatarUrl };
+
+ public bool HasPreviousMessage => EventsBefore.Last() is { Type: "m.room.message" } response && response.Sender == Event.Sender;
+
+
+
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
new file mode 100644
index 0000000..1213432
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineCanonicalAliasItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+ @if (previousEventContent is null) {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room alias to "@currentEventContent.Alias"</i>
+ }
+ else {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Alias" to "@currentEventContent.Alias"</i>
+ }
+}
+else {
+ <details>
+ <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ </details>
+}
+
+@code {
+ private RoomCanonicalAliasEventContent? previousEventContent => PreviousState?.TypedContent as RoomCanonicalAliasEventContent;
+
+ private RoomCanonicalAliasEventContent? currentEventContent => Event.TypedContent as RoomCanonicalAliasEventContent;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
new file mode 100644
index 0000000..172a38c
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineHistoryVisibilityItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+ @if (previousEventContent is null) {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the history visibility to "@currentEventContent.HistoryVisibility"</i>
+ }
+ else {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the history visibility from "@previousEventContent.HistoryVisibility" to "@currentEventContent.HistoryVisibility"</i>
+ }
+}
+else {
+ <details>
+ <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ </details>
+}
+
+@code {
+ private RoomHistoryVisibilityEventContent? previousEventContent => PreviousState?.TypedContent as RoomHistoryVisibilityEventContent;
+
+ private RoomHistoryVisibilityEventContent? currentEventContent => Event.TypedContent as RoomHistoryVisibilityEventContent;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
new file mode 100644
index 0000000..3b18b95
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -0,0 +1,53 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (roomMemberData is not null) {
+ @switch (roomMemberData.Membership) {
+ case "ban":
+ <i>@Event.StateKey was banned</i>
+ break;
+ case "invite":
+ <i>@Event.StateKey was invited</i>
+ break;
+ case "join" when Event.ReplacesState is not null:
+ <i>@Event.StateKey changed their display name to @(roomMemberData.DisplayName ?? Event.Sender)</i>
+ break;
+ case "join":
+ @if (prevRoomMemberData is null) {
+ <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> joined</i>
+ }
+ else {
+ <i><InlineUserItem User="@(new UserProfileResponse() { DisplayName = prevRoomMemberData.DisplayName, AvatarUrl = prevRoomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed their profile to <InlineUserItem User="@(new UserProfileResponse() { DisplayName = roomMemberData.DisplayName, AvatarUrl = roomMemberData.AvatarUrl })" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem></i>
+ }
+ break;
+ case "leave":
+ <i>@Event.StateKey left</i>
+ break;
+ case "knock":
+ <i>@Event.StateKey knocked</i>
+ break;
+ default:
+ <i>@Event.StateKey has an unknown state:</i>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ break;
+ }
+}
+else {
+ <details>
+ <summary>Unknown membership event for @Event.StateKey</summary>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ </details>
+}
+
+@code {
+
+ private RoomMemberEventContent? roomMemberData => Event.TypedContent as RoomMemberEventContent;
+ private RoomMemberEventContent? prevRoomMemberData => PreviousState?.TypedContent as RoomMemberEventContent;
+
+}
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
new file mode 100644
index 0000000..81956b0
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -0,0 +1,34 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec
+@inherits BaseTimelineItem
+
+<span>
+ @if (!HasPreviousMessage) {
+ <span><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem>:</span><br/>
+ }
+ @switch (currentEventContent.MessageType) {
+ case "m.text": {
+ @foreach (var line in currentEventContent.Body.Split('\n')) {
+ <span>@line</span><br/>
+ }
+ break;
+ }
+ case "m.image": {
+ <i>@currentEventContent.Body</i><br/>
+ <img src="@Homeserver.ResolveMediaUri(currentEventContent.Url)">
+ break;
+ }
+ default: {
+ <pre>
+ @Event.RawContent?.ToJson(indent: false)
+ </pre>
+ break;
+ }
+ }
+</span>
+
+@code {
+ private RoomMessageEventContent? previousEventContent => PreviousState?.TypedContent as RoomMessageEventContent;
+
+ private RoomMessageEventContent? currentEventContent => Event.TypedContent as RoomMessageEventContent;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
new file mode 100644
index 0000000..f3e6c7e
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomCreateItem.razor
@@ -0,0 +1,18 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@inherits BaseTimelineItem
+
+<i>
+ @Event.Sender created the room with room version @CreationEventContent.RoomVersion
+ @(CreationEventContent.Federate ?? true ? "and" : "without") federating with other servers.<br/>
+ This room is of type @(CreationEventContent.Type ?? "Untyped room (usually a chat room)")
+</i>
+<pre>
+ @Event.RawContent?.ToJson(indent: false)
+</pre>
+
+@code {
+
+ private RoomCreateEventContent CreationEventContent => Event.TypedContent as RoomCreateEventContent;
+
+}
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
new file mode 100644
index 0000000..eeec3de
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomNameItem.razor
@@ -0,0 +1,27 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+ @if (previousEventContent is null) {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room name to "@currentEventContent.Name"</i>
+ }
+ else {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room name from "@previousEventContent.Name" to "@currentEventContent.Name"</i>
+ }
+}
+else {
+ <details>
+ <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ </details>
+}
+
+@code {
+ private RoomNameEventContent? previousEventContent => PreviousState?.TypedContent as RoomNameEventContent;
+
+ private RoomNameEventContent? currentEventContent => Event.TypedContent as RoomNameEventContent;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
new file mode 100644
index 0000000..7ef17a8
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineRoomTopicItem.razor
@@ -0,0 +1,37 @@
+@using ArcaneLibs.Extensions
+@using LibMatrix.EventTypes.Spec.State
+@using LibMatrix.Responses
+@inherits BaseTimelineItem
+
+@if (currentEventContent is not null) {
+ @if (previousEventContent is null) {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> set the room topic to</i><br/>
+ <pre>
+ @currentEventContent.Topic
+ </pre>
+ }
+ else {
+ <i><InlineUserItem User="@CurrentSenderProfile" Homeserver="@Homeserver" UserId="@Event.StateKey"></InlineUserItem> changed the room topic from</i><br/>
+ <pre>
+ @previousEventContent.Topic
+ </pre><br/>
+ <i>to</i><br/>
+ <pre>
+ @currentEventContent.Topic
+ </pre>
+ }
+}
+else {
+ <details>
+ <summary>Unknown event @Event.Type (@Event.StateKey)</summary>
+ <pre>
+ @Event.ToJson()
+ </pre>
+ </details>
+}
+
+@code {
+ private RoomTopicEventContent? previousEventContent => PreviousState?.TypedContent as RoomTopicEventContent;
+
+ private RoomTopicEventContent? currentEventContent => Event.TypedContent as RoomTopicEventContent;
+}
\ No newline at end of file
diff --git a/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor b/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
new file mode 100644
index 0000000..4f05b30
--- /dev/null
+++ b/MatrixUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
@@ -0,0 +1,16 @@
+@using ArcaneLibs.Extensions
+@inherits BaseTimelineItem
+
+<div>
+ <details style="display: inline;">
+ <summary>
+ <i style="color: red;">Unknown event type: <pre style="display: inline;">@Event.Type</pre></i>
+ </summary>
+ <pre>@Event.ToJson(ignoreNull: true)</pre>
+ </details>
+</div>
+
+@code {
+
+
+}
|