a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[backend/razor] Drop custom NavLink as NavLinkMatch.All now behaves like AllExcludingQuery

+2 -245
+2 -1
Iceshrimp.Backend/Components/Generic/NavBarLink.razor
··· 1 1 @using Iceshrimp.Assets.PhosphorIcons 2 - <NavLink href="@Link.Href" class="nav-link" Match="NavLinkMatch.AllExcludingQuery" target="@Target"> 2 + @using Microsoft.AspNetCore.Components.Routing 3 + <NavLink href="@Link.Href" class="nav-link" Match="NavLinkMatch.All" target="@Target"> 3 4 @if (Link.Icon != null) 4 5 { 5 6 <Icon Name="Link.Icon"/>
-244
Iceshrimp.Backend/Components/Generic/NavLink.cs
··· 1 - using System.Diagnostics; 2 - using System.Globalization; 3 - using Microsoft.AspNetCore.Components; 4 - using Microsoft.AspNetCore.Components.Rendering; 5 - using Microsoft.AspNetCore.Components.Routing; 6 - 7 - namespace Iceshrimp.Backend.Components.Generic; 8 - 9 - /// <summary> 10 - /// A component that renders an anchor tag, automatically toggling its 'active' 11 - /// class based on whether its 'href' matches the current URI. 12 - /// </summary> 13 - public class NavLink : ComponentBase, IDisposable 14 - { 15 - private const string DefaultActiveClass = "active"; 16 - 17 - private bool _isActive; 18 - private string? _hrefAbsolute; 19 - private string? _class; 20 - 21 - /// <summary> 22 - /// Gets or sets the CSS class name applied to the NavLink when the 23 - /// current route matches the NavLink href. 24 - /// </summary> 25 - [Parameter] 26 - public string? ActiveClass { get; set; } 27 - 28 - /// <summary> 29 - /// Gets or sets a collection of additional attributes that will be added to the generated 30 - /// <c>a</c> element. 31 - /// </summary> 32 - [Parameter(CaptureUnmatchedValues = true)] 33 - public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; } 34 - 35 - /// <summary> 36 - /// Gets or sets the computed CSS class based on whether or not the link is active. 37 - /// </summary> 38 - protected string? CssClass { get; set; } 39 - 40 - /// <summary> 41 - /// Gets or sets the child content of the component. 42 - /// </summary> 43 - [Parameter] 44 - public RenderFragment? ChildContent { get; set; } 45 - 46 - /// <summary> 47 - /// Gets or sets a value representing the URL matching behavior. 48 - /// </summary> 49 - [Parameter] 50 - public NavLinkMatch Match { get; set; } 51 - 52 - [Inject] private NavigationManager NavigationManager { get; set; } = default!; 53 - 54 - /// <inheritdoc /> 55 - protected override void OnInitialized() 56 - { 57 - // We'll consider re-rendering on each location change 58 - NavigationManager.LocationChanged += OnLocationChanged; 59 - } 60 - 61 - /// <inheritdoc /> 62 - protected override void OnParametersSet() 63 - { 64 - // Update computed state 65 - string? href = null; 66 - if (AdditionalAttributes != null && AdditionalAttributes.TryGetValue("href", out var obj)) 67 - { 68 - href = Convert.ToString(obj, CultureInfo.InvariantCulture); 69 - } 70 - 71 - _hrefAbsolute = href == null ? null : NavigationManager.ToAbsoluteUri(href).AbsoluteUri; 72 - _isActive = ShouldMatch(NavigationManager.Uri); 73 - 74 - _class = null; 75 - if (AdditionalAttributes != null && AdditionalAttributes.TryGetValue("class", out obj)) 76 - { 77 - _class = Convert.ToString(obj, CultureInfo.InvariantCulture); 78 - } 79 - 80 - UpdateCssClass(); 81 - } 82 - 83 - /// <inheritdoc /> 84 - public void Dispose() 85 - { 86 - // To avoid leaking memory, it's important to detach any event handlers in Dispose() 87 - NavigationManager.LocationChanged -= OnLocationChanged; 88 - } 89 - 90 - private void UpdateCssClass() 91 - { 92 - CssClass = _isActive ? CombineWithSpace(_class, ActiveClass ?? DefaultActiveClass) : _class; 93 - } 94 - 95 - private void OnLocationChanged(object? sender, LocationChangedEventArgs args) 96 - { 97 - // We could just re-render always, but for this component we know the 98 - // only relevant state change is to the _isActive property. 99 - var shouldBeActiveNow = ShouldMatch(args.Location); 100 - if (shouldBeActiveNow != _isActive) 101 - { 102 - _isActive = shouldBeActiveNow; 103 - UpdateCssClass(); 104 - StateHasChanged(); 105 - } 106 - } 107 - 108 - private bool ShouldMatch(string currentUriAbsolute) 109 - { 110 - if (_hrefAbsolute == null) 111 - return false; 112 - if (EqualsHrefExactlyOrIfTrailingSlashAdded(currentUriAbsolute)) 113 - return true; 114 - if (Match == NavLinkMatch.AllExcludingQuery && EqualsHrefExcludingQuery(currentUriAbsolute)) 115 - return true; 116 - 117 - return Match == NavLinkMatch.Prefix && IsStrictlyPrefixWithSeparator(currentUriAbsolute, _hrefAbsolute); 118 - } 119 - 120 - private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute) 121 - { 122 - if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.OrdinalIgnoreCase)) 123 - { 124 - return true; 125 - } 126 - 127 - if (currentUriAbsolute.Length == _hrefAbsolute!.Length - 1) 128 - { 129 - // Special case: highlight links to http://host/path/ even if you're 130 - // at http://host/path (with no trailing slash) 131 - // 132 - // This is because the router accepts an absolute URI value of "same 133 - // as base URI but without trailing slash" as equivalent to "base URI", 134 - // which in turn is because it's common for servers to return the same page 135 - // for http://host/vdir as they do for host://host/vdir/ as it's no 136 - // good to display a blank page in that case. 137 - if (_hrefAbsolute[^1] == '/' 138 - && _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.OrdinalIgnoreCase)) 139 - { 140 - return true; 141 - } 142 - } 143 - 144 - return false; 145 - } 146 - 147 - private bool EqualsHrefExcludingQuery(string currentUriAbsolute) 148 - { 149 - Debug.Assert(_hrefAbsolute != null); 150 - 151 - currentUriAbsolute = currentUriAbsolute.Split('?')[0]; 152 - 153 - if (string.Equals(currentUriAbsolute, _hrefAbsolute, StringComparison.OrdinalIgnoreCase)) 154 - { 155 - return true; 156 - } 157 - 158 - if (currentUriAbsolute.Length == _hrefAbsolute.Length - 1) 159 - { 160 - // Special case: highlight links to http://host/path/ even if you're 161 - // at http://host/path (with no trailing slash) 162 - // 163 - // This is because the router accepts an absolute URI value of "same 164 - // as base URI but without trailing slash" as equivalent to "base URI", 165 - // which in turn is because it's common for servers to return the same page 166 - // for http://host/vdir as they do for host://host/vdir/ as it's no 167 - // good to display a blank page in that case. 168 - if (_hrefAbsolute[^1] == '/' 169 - && _hrefAbsolute.StartsWith(currentUriAbsolute, StringComparison.OrdinalIgnoreCase)) 170 - { 171 - return true; 172 - } 173 - } 174 - 175 - return false; 176 - } 177 - 178 - /// <inheritdoc/> 179 - protected override void BuildRenderTree(RenderTreeBuilder builder) 180 - { 181 - builder.OpenElement(0, "a"); 182 - 183 - builder.AddMultipleAttributes(1, AdditionalAttributes); 184 - builder.AddAttribute(2, "class", CssClass); 185 - if (_isActive) 186 - { 187 - builder.AddAttribute(3, "aria-current", "page"); 188 - } 189 - 190 - builder.AddContent(4, ChildContent); 191 - 192 - builder.CloseElement(); 193 - } 194 - 195 - private static string CombineWithSpace(string? str1, string str2) => str1 == null ? str2 : $"{str1} {str2}"; 196 - 197 - private static bool IsStrictlyPrefixWithSeparator(string value, string prefix) 198 - { 199 - var prefixLength = prefix.Length; 200 - if (value.Length > prefixLength) 201 - { 202 - return value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) 203 - && ( 204 - // Only match when there's a separator character either at the end of the 205 - // prefix or right after it. 206 - // Example: "/abc" is treated as a prefix of "/abc/def" but not "/abcdef" 207 - // Example: "/abc/" is treated as a prefix of "/abc/def" but not "/abcdef" 208 - prefixLength == 0 209 - || !IsUnreservedCharacter(prefix[prefixLength - 1]) 210 - || !IsUnreservedCharacter(value[prefixLength]) 211 - ); 212 - } 213 - 214 - return false; 215 - } 216 - 217 - private static bool IsUnreservedCharacter(char c) 218 - { 219 - // Checks whether it is an unreserved character according to 220 - // https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 221 - // Those are characters that are allowed in a URI but do not have a reserved 222 - // purpose (e.g. they do not separate the components of the URI) 223 - return char.IsLetterOrDigit(c) || c == '-' || c == '.' || c == '_' || c == '~'; 224 - } 225 - } 226 - 227 - /// <summary> 228 - /// Modifies the URL matching behavior for a <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" />. 229 - /// </summary> 230 - public enum NavLinkMatch 231 - { 232 - /// <summary> 233 - /// Specifies that the <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" /> should be active when it matches any prefix 234 - /// of the current URL. 235 - /// </summary> 236 - Prefix, 237 - 238 - /// <summary> 239 - /// Specifies that the <see cref="T:Microsoft.AspNetCore.Components.Routing.NavLink" /> should be active when it matches the entire 240 - /// current URL. 241 - /// </summary> 242 - All, 243 - AllExcludingQuery 244 - }