Send feature for mobile (#1256)

* Send feature for mobile

* added fallback for KdfIterations

* additional property exclusions for tests

* support encryptedFileData as byte array comparison in SendServiceTests

* formatting

* requested changes

* additional changes

* change position of send service registration to match declaration order
This commit is contained in:
Matt Portune 2021-02-10 19:50:10 -05:00 committed by GitHub
parent 52ba9f2ba7
commit a18e59a28a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 3776 additions and 33 deletions

View file

@ -123,7 +123,9 @@
<Compile Include="Receivers\LockAlarmReceiver.cs" /> <Compile Include="Receivers\LockAlarmReceiver.cs" />
<Compile Include="Receivers\PackageReplacedReceiver.cs" /> <Compile Include="Receivers\PackageReplacedReceiver.cs" />
<Compile Include="Renderers\CipherViewCellRenderer.cs" /> <Compile Include="Renderers\CipherViewCellRenderer.cs" />
<Compile Include="Renderers\ExtendedDatePickerRenderer.cs" />
<Compile Include="Renderers\CustomTabbedRenderer.cs" /> <Compile Include="Renderers\CustomTabbedRenderer.cs" />
<Compile Include="Renderers\ExtendedTimePickerRenderer.cs" />
<Compile Include="Renderers\ExtendedSliderRenderer.cs" /> <Compile Include="Renderers\ExtendedSliderRenderer.cs" />
<Compile Include="Renderers\CustomEditorRenderer.cs" /> <Compile Include="Renderers\CustomEditorRenderer.cs" />
<Compile Include="Renderers\CustomPickerRenderer.cs" /> <Compile Include="Renderers\CustomPickerRenderer.cs" />
@ -131,6 +133,7 @@
<Compile Include="Renderers\CustomSearchBarRenderer.cs" /> <Compile Include="Renderers\CustomSearchBarRenderer.cs" />
<Compile Include="Renderers\ExtendedListViewRenderer.cs" /> <Compile Include="Renderers\ExtendedListViewRenderer.cs" />
<Compile Include="Renderers\HybridWebViewRenderer.cs" /> <Compile Include="Renderers\HybridWebViewRenderer.cs" />
<Compile Include="Renderers\SendViewCellRenderer.cs" />
<Compile Include="Services\AndroidPushNotificationService.cs" /> <Compile Include="Services\AndroidPushNotificationService.cs" />
<Compile Include="Services\AndroidLogService.cs" /> <Compile Include="Services\AndroidLogService.cs" />
<Compile Include="MainApplication.cs" /> <Compile Include="MainApplication.cs" />
@ -176,6 +179,7 @@
<AndroidResource Include="Resources\drawable\login.xml" /> <AndroidResource Include="Resources\drawable\login.xml" />
<AndroidResource Include="Resources\drawable\logo.xml" /> <AndroidResource Include="Resources\drawable\logo.xml" />
<AndroidResource Include="Resources\drawable\logo_white.xml" /> <AndroidResource Include="Resources\drawable\logo_white.xml" />
<AndroidResource Include="Resources\drawable\paper_plane.xml" />
<AndroidResource Include="Resources\drawable\pencil.xml" /> <AndroidResource Include="Resources\drawable\pencil.xml" />
<AndroidResource Include="Resources\drawable\plus.xml" /> <AndroidResource Include="Resources\drawable\plus.xml" />
<AndroidResource Include="Resources\drawable\refresh.xml" /> <AndroidResource Include="Resources\drawable\refresh.xml" />
@ -183,6 +187,7 @@
<AndroidResource Include="Resources\drawable\shield.xml" /> <AndroidResource Include="Resources\drawable\shield.xml" />
<AndroidResource Include="Resources\drawable-v23\splash_screen.xml" /> <AndroidResource Include="Resources\drawable-v23\splash_screen.xml" />
<AndroidResource Include="Resources\drawable-v23\splash_screen_dark.xml" /> <AndroidResource Include="Resources\drawable-v23\splash_screen_dark.xml" />
<AndroidResource Include="Resources\layout\SendViewCell.axml" />
<AndroidResource Include="Resources\layout\Tabbar.axml" /> <AndroidResource Include="Resources\layout\Tabbar.axml" />
<AndroidResource Include="Resources\layout\Toolbar.axml" /> <AndroidResource Include="Resources\layout\Toolbar.axml" />
<AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" /> <AndroidResource Include="Resources\mipmap-anydpi-v26\ic_launcher.xml" />

View file

@ -0,0 +1,50 @@
using System.ComponentModel;
using Android.Content;
using Android.Views;
using Bit.App.Controls;
using Bit.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(ExtendedDatePicker), typeof(ExtendedDatePickerRenderer))]
namespace Bit.Droid.Renderers
{
public class ExtendedDatePickerRenderer : DatePickerRenderer
{
public ExtendedDatePickerRenderer(Context context)
: base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
{
base.OnElementChanged(e);
if (Control != null && Element is ExtendedDatePicker element)
{
// center text
Control.Gravity = GravityFlags.CenterHorizontal;
// use placeholder until NullableDate set
if (!element.NullableDate.HasValue)
{
Control.Text = element.PlaceHolder;
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == DatePicker.DateProperty.PropertyName ||
e.PropertyName == DatePicker.FormatProperty.PropertyName)
{
if (Control != null && Element is ExtendedDatePicker element)
{
if (Element.Format == element.PlaceHolder)
{
Control.Text = element.PlaceHolder;
return;
}
}
}
base.OnElementPropertyChanged(sender, e);
}
}
}

View file

@ -0,0 +1,50 @@
using System.ComponentModel;
using Android.Content;
using Android.Views;
using Bit.App.Controls;
using Bit.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(ExtendedTimePicker), typeof(ExtendedTimePickerRenderer))]
namespace Bit.Droid.Renderers
{
public class ExtendedTimePickerRenderer : TimePickerRenderer
{
public ExtendedTimePickerRenderer(Context context)
: base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
{
base.OnElementChanged(e);
if (Control != null && Element is ExtendedTimePicker element)
{
// center text
Control.Gravity = GravityFlags.CenterHorizontal;
// use placeholder until NullableTime set
if (!element.NullableTime.HasValue)
{
Control.Text = element.PlaceHolder;
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName ||
e.PropertyName == TimePicker.FormatProperty.PropertyName)
{
if (Control != null && Element is ExtendedTimePicker element)
{
if (Element.Format == element.PlaceHolder)
{
Control.Text = element.PlaceHolder;
return;
}
}
}
base.OnElementPropertyChanged(sender, e);
}
}
}

View file

@ -0,0 +1,199 @@
using System;
using System.ComponentModel;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Util;
using Android.Views;
using Android.Widget;
using Bit.App.Controls;
using Bit.App.Utilities;
using Bit.Droid.Renderers;
using FFImageLoading.Work;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Button = Android.Widget.Button;
using Color = Android.Graphics.Color;
using View = Android.Views.View;
[assembly: ExportRenderer(typeof(SendViewCell), typeof(SendViewCellRenderer))]
namespace Bit.Droid.Renderers
{
public class SendViewCellRenderer : ViewCellRenderer
{
private static Typeface _faTypeface;
private static Typeface _miTypeface;
private static Color _textColor;
private static Color _mutedColor;
private static Color _disabledIconColor;
private static bool _usingLightTheme;
private AndroidSendCell _cell;
protected override View GetCellCore(Cell item, View convertView,
ViewGroup parent, Context context)
{
// TODO expand beyond light/dark detection once we support custom theme switching without app restart
var themeChanged = _usingLightTheme != ThemeManager.UsingLightTheme;
if (_faTypeface == null)
{
_faTypeface = Typeface.CreateFromAsset(context.Assets, "FontAwesome.ttf");
}
if (_miTypeface == null)
{
_miTypeface = Typeface.CreateFromAsset(context.Assets, "MaterialIcons_Regular.ttf");
}
if (_textColor == default(Color) || themeChanged)
{
_textColor = ThemeManager.GetResourceColor("TextColor").ToAndroid();
}
if (_mutedColor == default(Color) || themeChanged)
{
_mutedColor = ThemeManager.GetResourceColor("MutedColor").ToAndroid();
}
if (_disabledIconColor == default(Color) || themeChanged)
{
_disabledIconColor = ThemeManager.GetResourceColor("DisabledIconColor").ToAndroid();
}
_usingLightTheme = ThemeManager.UsingLightTheme;
var sendCell = item as SendViewCell;
_cell = convertView as AndroidSendCell;
if (_cell == null)
{
_cell = new AndroidSendCell(context, sendCell, _faTypeface, _miTypeface);
}
else
{
_cell.SendViewCell.PropertyChanged -= CellPropertyChanged;
}
sendCell.PropertyChanged += CellPropertyChanged;
_cell.UpdateCell(sendCell);
_cell.UpdateColors(_textColor, _mutedColor, _disabledIconColor);
return _cell;
}
public void CellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var sendCell = sender as SendViewCell;
_cell.SendViewCell = sendCell;
if (e.PropertyName == SendViewCell.SendProperty.PropertyName)
{
_cell.UpdateCell(sendCell);
}
}
}
public class AndroidSendCell : LinearLayout, INativeElementView
{
private readonly Typeface _faTypeface;
private readonly Typeface _miTypeface;
private IScheduledWork _currentTask;
public AndroidSendCell(Context context, SendViewCell sendView, Typeface faTypeface, Typeface miTypeface)
: base(context)
{
SendViewCell = sendView;
_faTypeface = faTypeface;
_miTypeface = miTypeface;
var view = (context as Activity).LayoutInflater.Inflate(Resource.Layout.SendViewCell, null);
Icon = view.FindViewById<TextView>(Resource.Id.SendCellIcon);
Name = view.FindViewById<TextView>(Resource.Id.SendCellName);
SubTitle = view.FindViewById<TextView>(Resource.Id.SendCellSubTitle);
HasPasswordIcon = view.FindViewById<TextView>(Resource.Id.SendCellHasPasswordIcon);
MaxAccessCountReachedIcon = view.FindViewById<TextView>(Resource.Id.SendCellMaxAccessCountReachedIcon);
ExpiredIcon = view.FindViewById<TextView>(Resource.Id.SendCellExpiredIcon);
PendingDeleteIcon = view.FindViewById<TextView>(Resource.Id.SendCellPendingDeleteIcon);
MoreButton = view.FindViewById<Button>(Resource.Id.SendCellButton);
MoreButton.Click += MoreButton_Click;
Icon.Typeface = _faTypeface;
HasPasswordIcon.Typeface = _faTypeface;
MaxAccessCountReachedIcon.Typeface = _faTypeface;
ExpiredIcon.Typeface = _faTypeface;
PendingDeleteIcon.Typeface = _faTypeface;
MoreButton.Typeface = _miTypeface;
var small = (float)Device.GetNamedSize(NamedSize.Small, typeof(Label));
Icon.SetTextSize(ComplexUnitType.Pt, 10);
Name.SetTextSize(ComplexUnitType.Sp, (float)Device.GetNamedSize(NamedSize.Medium, typeof(Label)));
SubTitle.SetTextSize(ComplexUnitType.Sp, small);
HasPasswordIcon.SetTextSize(ComplexUnitType.Sp, small);
MaxAccessCountReachedIcon.SetTextSize(ComplexUnitType.Sp, small);
ExpiredIcon.SetTextSize(ComplexUnitType.Sp, small);
PendingDeleteIcon.SetTextSize(ComplexUnitType.Sp, small);
MoreButton.SetTextSize(ComplexUnitType.Sp, 25);
AddView(view);
}
public SendViewCell SendViewCell { get; set; }
public Element Element => SendViewCell;
public TextView Icon { get; set; }
public TextView Name { get; set; }
public TextView SubTitle { get; set; }
public TextView HasPasswordIcon { get; set; }
public TextView MaxAccessCountReachedIcon { get; set; }
public TextView ExpiredIcon { get; set; }
public TextView PendingDeleteIcon { get; set; }
public Button MoreButton { get; set; }
public void UpdateCell(SendViewCell sendCell)
{
UpdateIconImage(sendCell);
var send = sendCell.Send;
Name.Text = send.Name;
SubTitle.Text = send.DisplayDate;
HasPasswordIcon.Visibility = send.HasPassword ? ViewStates.Visible : ViewStates.Gone;
MaxAccessCountReachedIcon.Visibility = send.MaxAccessCountReached ? ViewStates.Visible : ViewStates.Gone;
ExpiredIcon.Visibility = send.Expired ? ViewStates.Visible : ViewStates.Gone;
PendingDeleteIcon.Visibility = send.PendingDelete ? ViewStates.Visible : ViewStates.Gone;
}
public void UpdateIconImage(SendViewCell sendCell)
{
if (_currentTask != null && !_currentTask.IsCancelled && !_currentTask.IsCompleted)
{
_currentTask.Cancel();
}
var send = sendCell.Send;
var iconImage = sendCell.GetIconImage(send);
Icon.Text = iconImage;
}
public void UpdateColors(Color textColor, Color mutedColor,
Color iconDisabledColor)
{
Name.SetTextColor(textColor);
SubTitle.SetTextColor(mutedColor);
Icon.SetTextColor(mutedColor);
HasPasswordIcon.SetTextColor(mutedColor);
MaxAccessCountReachedIcon.SetTextColor(mutedColor);
ExpiredIcon.SetTextColor(mutedColor);
PendingDeleteIcon.SetTextColor(mutedColor);
MoreButton.SetTextColor(iconDisabledColor);
}
private void MoreButton_Click(object sender, EventArgs e)
{
if (SendViewCell.ButtonCommand?.CanExecute(SendViewCell.Send) ?? false)
{
SendViewCell.ButtonCommand.Execute(SendViewCell.Send);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
MoreButton.Click -= MoreButton_Click;
}
base.Dispose(disposing);
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:fillColor="#FF000000"
android:pathData="M508.757,52.805L68.978,306.52C51.804,316.388 53.987,340.299 71.065,347.51L171.925,389.827L444.522,149.585C449.741,144.936 457.141,152.052 452.682,157.46L224.111,435.94L224.111,512.32C224.111,534.712 251.152,543.536 264.436,527.312L324.686,453.967L442.909,503.496C456.382,509.189 471.753,500.744 474.22,486.227L542.536,76.336C545.762,57.17 525.172,43.317 508.757,52.805Z" />
</vector>

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="44dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="2.2dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="39.8dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center">
<TextView
android:id="@+id/SendCellIcon"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_gravity="center"
android:gravity="center"
android:text="[X]" />
</LinearLayout>
<LinearLayout
android:id="@+id/SendCellContent"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:gravity="center"
android:paddingVertical="7.65dp">
<LinearLayout
android:id="@+id/SendCellContentTop"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/SendCellName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:text="Name" />
<TextView
android:id="@+id/SendCellHasPasswordIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
android:singleLine="true"
android:text="&#xf084;" />
<TextView
android:id="@+id/SendCellMaxAccessCountReachedIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
android:singleLine="true"
android:text="&#xf05e;" />
<TextView
android:id="@+id/SendCellExpiredIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
android:singleLine="true"
android:text="&#xf017;" />
<TextView
android:id="@+id/SendCellPendingDeleteIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
android:singleLine="true"
android:text="&#xf1f8;" />
</LinearLayout>
<TextView
android:id="@+id/SendCellSubTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:text="SubTitle" />
</LinearLayout>
<Button
android:id="@+id/SendCellButton"
android:layout_width="37dp"
android:layout_height="match_parent"
android:text="&#xe5d4;"
android:gravity="center"
android:padding="0dp"
android:background="@android:color/transparent" />
</LinearLayout>
</RelativeLayout>

View file

@ -24,7 +24,6 @@
<item name="colorControlNormal">@color/border</item> <item name="colorControlNormal">@color/border</item>
<item name="android:navigationBarColor">@android:color/black</item> <item name="android:navigationBarColor">@android:color/black</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
<item name="android:colorActivatedHighlight">@android:color/transparent</item> <item name="android:colorActivatedHighlight">@android:color/transparent</item>
<item name="android:textCursorDrawable">@null</item> <item name="android:textCursorDrawable">@null</item>
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item> <item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
@ -47,7 +46,6 @@
<item name="colorControlNormal">@color/dark_border</item> <item name="colorControlNormal">@color/dark_border</item>
<item name="android:navigationBarColor">@color/dark_navigationBarBackground</item> <item name="android:navigationBarColor">@color/dark_navigationBarBackground</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
<item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
<item name="android:colorActivatedHighlight">@android:color/transparent</item> <item name="android:colorActivatedHighlight">@android:color/transparent</item>
<item name="android:textCursorDrawable">@null</item> <item name="android:textCursorDrawable">@null</item>
<item name="popupTheme">@style/ThemeOverlay.AppCompat</item> <item name="popupTheme">@style/ThemeOverlay.AppCompat</item>
@ -95,9 +93,4 @@
<item name="android:colorBackground">@color/nord_popupBackground</item> <item name="android:colorBackground">@color/nord_popupBackground</item>
<item name="android:textColor">@color/nord_popupText</item> <item name="android:textColor">@color/nord_popupText</item>
</style> </style>
<!-- Other theme components -->
<style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog">
<item name="colorAccent">#FF4081</item>
</style>
</resources> </resources>

View file

@ -116,6 +116,10 @@
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon> <DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Update="Pages\Send\SendGroupingsPage\SendGroupingsPage.xaml.cs">
<DependentUpon>SendGroupingsPage.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -0,0 +1,86 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class ExtendedDatePicker : DatePicker
{
private string _format;
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
nameof(PlaceHolder), typeof(string), typeof(ExtendedDatePicker));
public string PlaceHolder
{
get { return (string)GetValue(PlaceHolderProperty); }
set { SetValue(PlaceHolderProperty, value); }
}
public static readonly BindableProperty NullableDateProperty = BindableProperty.Create(
nameof(NullableDate), typeof(DateTime?), typeof(ExtendedDatePicker));
public DateTime? NullableDate
{
get { return (DateTime?)GetValue(NullableDateProperty); }
set
{
SetValue(NullableDateProperty, value);
UpdateDate();
}
}
private void UpdateDate()
{
if (NullableDate.HasValue)
{
if (_format != null)
{
Format = _format;
}
}
else
{
Format = PlaceHolder;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
_format = Format;
UpdateDate();
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == DateProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
!IsFocused && (Date.ToString("d") ==
DateTime.Now.ToString("d"))))
{
NullableDate = Date;
UpdateDate();
}
if (propertyName == NullableDateProperty.PropertyName)
{
if (NullableDate.HasValue)
{
Date = NullableDate.Value;
if (Date.ToString(_format) == DateTime.Now.ToString(_format))
{
UpdateDate();
}
}
else
{
UpdateDate();
}
}
}
}
}

View file

@ -0,0 +1,86 @@
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class ExtendedTimePicker : TimePicker
{
private string _format;
public static readonly BindableProperty PlaceHolderProperty = BindableProperty.Create(
nameof(PlaceHolder), typeof(string), typeof(ExtendedTimePicker));
public string PlaceHolder
{
get { return (string)GetValue(PlaceHolderProperty); }
set { SetValue(PlaceHolderProperty, value); }
}
public static readonly BindableProperty NullableTimeProperty = BindableProperty.Create(
nameof(NullableTime), typeof(TimeSpan?), typeof(ExtendedTimePicker));
public TimeSpan? NullableTime
{
get { return (TimeSpan?)GetValue(NullableTimeProperty); }
set
{
SetValue(NullableTimeProperty, value);
UpdateTime();
}
}
private void UpdateTime()
{
if (NullableTime.HasValue)
{
if (_format != null)
{
Format = _format;
}
}
else
{
Format = PlaceHolder;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
_format = Format;
UpdateTime();
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TimeProperty.PropertyName || (propertyName == IsFocusedProperty.PropertyName &&
!IsFocused && (Time.ToString("t") ==
DateTime.Now.TimeOfDay.ToString("t"))))
{
NullableTime = Time;
UpdateTime();
}
if (propertyName == NullableTimeProperty.PropertyName)
{
if (NullableTime.HasValue)
{
Time = NullableTime.Value;
if (Time.ToString(_format) == DateTime.Now.TimeOfDay.ToString(_format))
{
UpdateTime();
}
}
else
{
UpdateTime();
}
}
}
}
}

View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Controls.SendViewCell"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities">
<Grid
x:Name="_grid"
StyleClass="list-row, list-row-platform"
RowSpacing="0"
ColumnSpacing="0"
x:DataType="controls:SendViewCellViewModel">
<Grid.BindingContext>
<controls:SendViewCellViewModel />
</Grid.BindingContext>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<controls:FaLabel
x:Name="_icon"
Grid.Row="0"
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
AutomationProperties.IsInAccessibleTree="False" />
<Grid RowSpacing="0" ColumnSpacing="0" Grid.Row="0" Grid.Column="1" VerticalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="0"
StyleClass="list-title, list-title-platform"
Text="{Binding Send.Name, Mode=OneWay}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="5"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Send.DisplayDate, Mode=OneWay}" />
<controls:FaLabel
Grid.Column="1"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="&#xf084;"
IsVisible="{Binding Send.HasPassword, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Password}" />
<controls:FaLabel
Grid.Column="2"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="&#xf05e;"
IsVisible="{Binding Send.MaxAccessCountReached, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n MaxAccessCountReached}" />
<controls:FaLabel
Grid.Column="3"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="&#xf017;"
IsVisible="{Binding Send.Expired, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Expired}" />
<controls:FaLabel
Grid.Column="4"
Grid.Row="0"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-title-icon"
Margin="5, 0, 0, 0"
Text="&#xf1f8;"
IsVisible="{Binding Send.PendingDelete, Mode=OneWay}"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n PendingDelete}" />
</Grid>
<controls:MiButton
Grid.Row="0"
Grid.Column="2"
Text="&#xe5d3;"
StyleClass="list-row-button, list-row-button-platform, btn-disabled"
Clicked="MoreButton_Clicked"
VerticalOptions="CenterAndExpand"
HorizontalOptions="EndAndExpand"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
</Grid>
</ViewCell>

View file

@ -0,0 +1,110 @@
using System;
using Bit.App.Pages;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class SendViewCell : ViewCell
{
public static readonly BindableProperty SendProperty = BindableProperty.Create(
nameof(Send), typeof(SendView), typeof(SendViewCell), default(SendView), BindingMode.OneWay);
public static readonly BindableProperty ButtonCommandProperty = BindableProperty.Create(
nameof(ButtonCommand), typeof(Command<SendView>), typeof(SendViewCell));
private readonly IEnvironmentService _environmentService;
private SendViewCellViewModel _viewModel;
private bool _usingNativeCell;
public SendViewCell()
{
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
if (Device.RuntimePlatform == Device.iOS)
{
InitializeComponent();
_viewModel = _grid.BindingContext as SendViewCellViewModel;
}
else
{
_usingNativeCell = true;
}
}
public SendView Send
{
get => GetValue(SendProperty) as SendView;
set => SetValue(SendProperty, value);
}
public Command<SendView> ButtonCommand
{
get => GetValue(ButtonCommandProperty) as Command<SendView>;
set => SetValue(ButtonCommandProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (_usingNativeCell)
{
return;
}
if (propertyName == SendProperty.PropertyName)
{
_viewModel.Send = Send;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (_usingNativeCell)
{
return;
}
SendView send = null;
if (BindingContext is SendGroupingsPageListItem sendGroupingsPageListItem)
{
send = sendGroupingsPageListItem.Send;
}
else if (BindingContext is SendView sv)
{
send = sv;
}
if (send != null)
{
var iconImage = GetIconImage(send);
_icon.IsVisible = true;
_icon.Text = iconImage;
}
}
public string GetIconImage(SendView send)
{
string icon = null;
switch (send.Type)
{
case SendType.Text:
icon = "\uf0f6"; // fa-file-text-o
break;
case SendType.File:
icon = "\uf016"; // fa-file-o
break;
default:
break;
}
return icon;
}
private void MoreButton_Clicked(object sender, EventArgs e)
{
ButtonCommand?.Execute(Send);
}
}
}

View file

@ -0,0 +1,16 @@
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Controls
{
public class SendViewCellViewModel : ExtendedViewModel
{
private SendView _send;
public SendView Send
{
get => _send;
set => SetProperty(ref _send, value);
}
}
}

View file

@ -4,6 +4,7 @@
{ {
public string Page { get; set; } public string Page { get; set; }
public string CipherId { get; set; } public string CipherId { get; set; }
public string SendId { get; set; }
public string SearchText { get; set; } public string SearchText { get; set; }
} }
} }

View file

@ -0,0 +1,340 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendAddEditPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:DataType="pages:SendAddEditPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SendAddEditPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Save}" Clicked="Save_Clicked" Order="Primary" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<u:IsNullConverter x:Key="null" />
<u:IsNotNullConverter x:Key="notNull" />
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Key="closeItem" x:Name="_closeItem" />
<ToolbarItem Icon="more_vert.png" Clicked="More_Clicked" Order="Primary" x:Name="_moreItem"
x:Key="moreItem"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Options}" />
<ToolbarItem Text="{u:I18n RemovePassword}"
Clicked="RemovePassword_Clicked"
Order="Secondary"
IsDestructive="True"
x:Name="_removePassword"
x:Key="removePassword" />
<ToolbarItem Text="{u:I18n CopyLink}"
Clicked="CopyLink_Clicked"
Order="Secondary"
IsDestructive="True"
x:Name="_copyLink"
x:Key="copyLink" />
<ToolbarItem Text="{u:I18n ShareLink}"
Clicked="ShareLink_Clicked"
Order="Secondary"
IsDestructive="True"
x:Name="_shareLink"
x:Key="shareLink" />
<ToolbarItem Text="{u:I18n Delete}"
Clicked="Delete_Clicked"
Order="Secondary"
IsDestructive="True"
x:Name="_deleteItem"
x:Key="deleteItem" />
<ScrollView x:Key="scrollView" x:Name="_scrollView">
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
<Label
Text="{u:I18n Type}"
StyleClass="box-label" />
<Picker
x:Name="_typePicker"
ItemsSource="{Binding TypeOptions, Mode=OneTime}"
SelectedIndex="{Binding TypeSelectedIndex}"
StyleClass="box-value" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
StyleClass="box-label" />
<Entry
x:Name="_nameEntry"
Text="{Binding Send.Name}"
StyleClass="box-value" />
</StackLayout>
<StackLayout StyleClass="box-row"
IsVisible="{Binding IsText}">
<Label
Text="{u:I18n TypeText}"
StyleClass="box-label" />
<Editor
x:Name="_textEditor"
AutoSize="TextChanges"
Text="{Binding Send.Text.Text}"
StyleClass="box-value"
Margin="{Binding EditorMargins}" />
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowEditorSeparators}" />
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n HideTextByDefault}"
StyleClass="box-label"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Text.Hidden}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
</StackLayout>
<StackLayout StyleClass="box-row"
IsVisible="{Binding IsFile}">
<Label
Text="{u:I18n TypeFile}"
StyleClass="box-label" />
<Label
Text="{Binding Send.File.FileName, Mode=OneWay}"
IsVisible="{Binding EditMode}"
StyleClass="box-value" />
<StackLayout
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
StyleClass="box-row">
<Label
IsVisible="{Binding FileName, Converter={StaticResource null}}"
Text="{u:I18n NoFileChosen}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Label
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
Text="{Binding FileName}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Button Text="{u:I18n ChooseFile}"
StyleClass="box-button-row"
Clicked="ChooseFile_Clicked" />
<Label
Margin="0, 10, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout>
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n Options, Header=True}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n DeletionDate}"
StyleClass="box-label" />
<Picker
x:Name="_deletionDateTypePicker"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding DeletionTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding DeletionDateTypeSelectedIndex}"
StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}" />
<Grid
IsVisible="{Binding ShowDeletionCustomPickers}"
Margin="0,5,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ExtendedDatePicker
Date="{Binding DeletionDate}"
Format="d"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
Time="{Binding DeletionTime}"
Format="t"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n DeletionTime}"
Grid.Column="1" />
</Grid>
<Label
Text="{u:I18n DeletionDateInfo}"
StyleClass="box-label"
Margin="0,5,0,0" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n ExpirationDate}"
StyleClass="box-label" />
<Picker
x:Name="_expirationDateTypePicker"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}"
ItemsSource="{Binding ExpirationTypeOptions, Mode=OneTime}"
SelectedIndex="{Binding ExpirationDateTypeSelectedIndex}"
StyleClass="box-value"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}" />
<Grid
IsVisible="{Binding ShowExpirationCustomPickers}"
Margin="0,5,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ExtendedDatePicker
NullableDate="{Binding ExpirationDate, Mode=TwoWay}"
PlaceHolder="mm/dd/yyyy"
Format="d"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationDate}"
Grid.Column="0" />
<controls:ExtendedTimePicker
NullableTime="{Binding ExpirationTime, Mode=TwoWay}"
PlaceHolder="--:-- --"
Format="t"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ExpirationTime}"
Grid.Column="1" />
</Grid>
<StackLayout Orientation="Horizontal" Margin="0,5,0,0">
<Label
Text="{u:I18n ExpirationDateInfo}"
StyleClass="box-label"
HorizontalOptions="StartAndExpand"
VerticalOptions="Center" />
<Button
Text="{u:I18n Clear}"
IsVisible="{Binding EditMode}"
WidthRequest="110"
StyleClass="box-row-button"
Clicked="ClearExpirationDate_Clicked" />
</StackLayout>
</StackLayout>
<BoxView StyleClass="box-row-separator"
Margin="0, 5, 0, 0" />
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n MaximumAccessCount}"
StyleClass="box-label" />
<StackLayout StyleClass="box-row" Orientation="Horizontal">
<Entry
Text="{Binding MaxAccessCount}"
StyleClass="box-value"
Keyboard="Numeric"
MaxLength="9"
TextChanged="OnMaxAccessCountTextChanged"
HorizontalOptions="FillAndExpand" />
<Stepper
x:Name="_maxAccessCountStepper"
Value="{Binding MaxAccessCount}"
Maximum="999999999"
Margin="10,0,0,0" />
</StackLayout>
<Label
Text="{u:I18n MaximumAccessCountInfo}"
StyleClass="box-label"
Margin="0,5" />
<StackLayout
IsVisible="{Binding EditMode}"
StyleClass="box-row"
Orientation="Horizontal">
<Label
Text="{u:I18n CurrentAccessCount}"
StyleClass="box-label"
HorizontalOptions="StartAndExpand"
VerticalTextAlignment="Center" />
<Label
Text="{Binding Send.AccessCount, Mode=OneWay}"
StyleClass="box-label"
HorizontalOptions="End" />
</StackLayout>
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row" Margin="0,5">
<Label
Text="{u:I18n NewPassword}"
StyleClass="box-label" />
<StackLayout Orientation="Horizontal">
<Entry
Text="{Binding NewPassword}"
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
StyleClass="box-value"
IsSpellCheckEnabled="False"
IsTextPredictionEnabled="False"
HorizontalOptions="FillAndExpand" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
Command="{Binding TogglePasswordCommand}"
Margin="10,0,0,0"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
</StackLayout>
<Label
Text="{u:I18n PasswordInfo}"
StyleClass="box-label"
Margin="0,5,0,0" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row" Margin="0,5">
<Label
Text="{u:I18n Notes}"
StyleClass="box-label" />
<Editor
AutoSize="TextChanges"
Text="{Binding Send.Notes}"
StyleClass="box-value"
Margin="{Binding EditorMargins}" />
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowEditorSeparators}" />
<Label
Text="{u:I18n NotesInfo}"
StyleClass="box-label"
Margin="0,5,0,0" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n DisableSend}"
StyleClass="box-label"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Send.Disabled}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-switch">
<Label
Text="{u:I18n ShareOnSave}"
StyleClass="box-label"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding ShareOnSave}"
HorizontalOptions="End"
Margin="10,0,0,0" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
</StackLayout>
</StackLayout>
</ScrollView>
</ResourceDictionary>
</ContentPage.Resources>
</pages:BaseContentPage>

View file

@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using Bit.App.Resources;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
using Entry = Xamarin.Forms.Entry;
namespace Bit.App.Pages
{
public partial class SendAddEditPage : BaseContentPage
{
private readonly IBroadcasterService _broadcasterService;
private SendAddEditPageViewModel _vm;
public SendAddEditPage(
string sendId = null,
SendType? type = null)
{
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
InitializeComponent();
_vm = BindingContext as SendAddEditPageViewModel;
_vm.Page = this;
_vm.SendId = sendId;
_vm.Type = type;
_vm.Init();
if (Device.RuntimePlatform == Device.Android)
{
if (_vm.EditMode)
{
ToolbarItems.Add(_removePassword);
ToolbarItems.Add(_copyLink);
ToolbarItems.Add(_shareLink);
ToolbarItems.Add(_deleteItem);
}
_vm.EditorMargins = new Thickness(0, 5, 0, 0);
}
else if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem);
if (_vm.EditMode)
{
ToolbarItems.Add(_moreItem);
}
_vm.ShowEditorSeparators = true;
_vm.EditorMargins = new Thickness(0, 10, 0, 5);
_typePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_deletionDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
_expirationDateTypePicker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);
}
_typePicker.ItemDisplayBinding = new Binding("Key");
_deletionDateTypePicker.ItemDisplayBinding = new Binding("Key");
_expirationDateTypePicker.ItemDisplayBinding = new Binding("Key");
if (_vm.IsText)
{
_nameEntry.ReturnType = ReturnType.Next;
_nameEntry.ReturnCommand = new Command(() => _textEditor.Focus());
}
}
protected override async void OnAppearing()
{
base.OnAppearing();
_broadcasterService.Subscribe(nameof(SendAddEditPage), message =>
{
if (message.Command == "selectFileResult")
{
Device.BeginInvokeOnMainThread(() =>
{
var data = message.Data as Tuple<byte[], string>;
_vm.FileData = data.Item1;
_vm.FileName = data.Item2;
});
}
});
await LoadOnAppearedAsync(_scrollView, true, async () =>
{
var success = await _vm.LoadAsync();
if (!success)
{
await Navigation.PopModalAsync();
return;
}
if (!_vm.EditMode && string.IsNullOrWhiteSpace(_vm.Send?.Name))
{
RequestFocus(_nameEntry);
}
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
if (Device.RuntimePlatform != Device.iOS)
{
_broadcasterService.Unsubscribe(nameof(SendAddEditPage));
}
}
private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.NewTextValue))
{
_vm.MaxAccessCount = null;
_maxAccessCountStepper.Value = 0;
return;
}
// accept only digits
if (!int.TryParse(e.NewTextValue, out int _))
{
((Entry)sender).Text = e.OldTextValue;
}
}
private async void ChooseFile_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.ChooseFileAsync();
}
}
private void ClearExpirationDate_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ClearExpirationDate();
}
}
private async void Save_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.SubmitAsync();
}
}
private async void RemovePassword_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.RemovePasswordAsync();
}
}
private async void CopyLink_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.CopyLinkAsync();
}
}
private async void ShareLink_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await _vm.ShareLinkAsync();
}
}
private async void Delete_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
if (await _vm.DeleteAsync())
{
await Navigation.PopModalAsync();
}
}
}
private async void More_Clicked(object sender, EventArgs e)
{
if (!DoOnce())
{
return;
}
var options = new List<string>();
if (_vm.Send.HasPassword)
{
options.Add(AppResources.RemovePassword);
}
if (_vm.EditMode)
{
options.Add(AppResources.CopyLink);
options.Add(AppResources.ShareLink);
}
var selection = await DisplayActionSheet(AppResources.Options, AppResources.Cancel,
_vm.EditMode ? AppResources.Delete : null, options.ToArray());
if (selection == AppResources.RemovePassword)
{
await _vm.RemovePasswordAsync();
}
else if (selection == AppResources.CopyLink)
{
await _vm.CopyLinkAsync();
}
else if (selection == AppResources.ShareLink)
{
await _vm.ShareLinkAsync();
}
else if (selection == AppResources.Delete)
{
if (await _vm.DeleteAsync())
{
await Navigation.PopModalAsync();
}
}
}
private async void Close_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
await Navigation.PopModalAsync();
}
}
}
}

View file

@ -0,0 +1,510 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class SendAddEditPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly ISendService _sendService;
private bool _canAccessPremium;
private SendView _send;
private bool _showEditorSeparators;
private Thickness _editorMargins;
private bool _showPassword;
private int _typeSelectedIndex;
private int _deletionDateTypeSelectedIndex;
private int _expirationDateTypeSelectedIndex;
private DateTime _deletionDate;
private TimeSpan _deletionTime;
private DateTime? _expirationDate;
private TimeSpan? _expirationTime;
private bool _isOverridingPickers;
private int? _maxAccessCount;
private string[] _additionalSendProperties = new []
{
nameof(IsText),
nameof(IsFile),
};
public SendAddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
TogglePasswordCommand = new Command(TogglePassword);
TypeOptions = new List<KeyValuePair<string, SendType>>
{
new KeyValuePair<string, SendType>(AppResources.TypeText, SendType.Text),
new KeyValuePair<string, SendType>(AppResources.TypeFile, SendType.File),
};
DeletionTypeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
};
ExpirationTypeOptions = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(AppResources.Never, AppResources.Never),
new KeyValuePair<string, string>(AppResources.OneHour, AppResources.OneHour),
new KeyValuePair<string, string>(AppResources.OneDay, AppResources.OneDay),
new KeyValuePair<string, string>(AppResources.TwoDays, AppResources.TwoDays),
new KeyValuePair<string, string>(AppResources.ThreeDays, AppResources.ThreeDays),
new KeyValuePair<string, string>(AppResources.SevenDays, AppResources.SevenDays),
new KeyValuePair<string, string>(AppResources.ThirtyDays, AppResources.ThirtyDays),
new KeyValuePair<string, string>(AppResources.Custom, AppResources.Custom),
};
}
public Command TogglePasswordCommand { get; set; }
public string SendId { get; set; }
public SendType? Type { get; set; }
public string FileName { get; set; }
public byte[] FileData { get; set; }
public string NewPassword { get; set; }
public bool ShareOnSave { get; set; }
public List<KeyValuePair<string, SendType>> TypeOptions { get; }
public List<KeyValuePair<string, string>> DeletionTypeOptions { get; }
public List<KeyValuePair<string, string>> ExpirationTypeOptions { get; }
public int TypeSelectedIndex
{
get => _typeSelectedIndex;
set
{
if (SetProperty(ref _typeSelectedIndex, value))
{
TypeChanged();
}
}
}
public int DeletionDateTypeSelectedIndex
{
get => _deletionDateTypeSelectedIndex;
set
{
if (SetProperty(ref _deletionDateTypeSelectedIndex, value))
{
DeletionTypeChanged();
}
}
}
public DateTime DeletionDate
{
get => _deletionDate;
set => SetProperty(ref _deletionDate, value);
}
public TimeSpan DeletionTime
{
get => _deletionTime;
set => SetProperty(ref _deletionTime, value);
}
public int ExpirationDateTypeSelectedIndex
{
get => _expirationDateTypeSelectedIndex;
set
{
if (SetProperty(ref _expirationDateTypeSelectedIndex, value))
{
ExpirationTypeChanged();
}
}
}
public DateTime? ExpirationDate
{
get => _expirationDate;
set
{
if (SetProperty(ref _expirationDate, value))
{
ExpirationDateChanged();
}
}
}
public TimeSpan? ExpirationTime
{
get => _expirationTime;
set
{
if (SetProperty(ref _expirationTime, value))
{
ExpirationTimeChanged();
}
}
}
public int? MaxAccessCount
{
get => _maxAccessCount;
set
{
if (SetProperty(ref _maxAccessCount, value))
{
MaxAccessCountChanged();
}
}
}
public SendView Send
{
get => _send;
set => SetProperty(ref _send, value, additionalPropertyNames: _additionalSendProperties);
}
public bool ShowEditorSeparators
{
get => _showEditorSeparators;
set => SetProperty(ref _showEditorSeparators, value);
}
public Thickness EditorMargins
{
get => _editorMargins;
set => SetProperty(ref _editorMargins, value);
}
public bool ShowPassword
{
get => _showPassword;
set => SetProperty(ref _showPassword, value,
additionalPropertyNames: new []
{
nameof(ShowPasswordIcon)
});
}
public bool EditMode => !string.IsNullOrWhiteSpace(SendId);
public bool IsText => Send?.Type == SendType.Text;
public bool IsFile => Send?.Type == SendType.File;
public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6;
public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7;
public string ShowPasswordIcon => ShowPassword ? "" : "";
public void Init()
{
PageTitle = EditMode ? AppResources.EditSend : AppResources.AddSend;
}
public async Task<bool> LoadAsync()
{
var userService = ServiceContainer.Resolve<IUserService>("userService");
_canAccessPremium = await userService.CanAccessPremiumAsync();
// TODO Policy Check
if (Send == null)
{
if (EditMode)
{
var send = await _sendService.GetAsync(SendId);
if (send == null)
{
return false;
}
Send = await send.DecryptAsync();
}
else
{
Send = new SendView
{
Type = Type.GetValueOrDefault(SendType.Text),
DeletionDate = DateTime.Now.AddDays(7),
};
DeletionDateTypeSelectedIndex = 4;
ExpirationDateTypeSelectedIndex = 0;
}
TypeSelectedIndex = TypeOptions.FindIndex(k => k.Value == Send.Type);
MaxAccessCount = Send.MaxAccessCount;
_isOverridingPickers = true;
DeletionDate = Send.DeletionDate.ToLocalTime();
DeletionTime = DeletionDate.TimeOfDay;
ExpirationDate = Send.ExpirationDate?.ToLocalTime();
ExpirationTime = ExpirationDate?.TimeOfDay;
_isOverridingPickers = false;
}
return true;
}
public async Task ChooseFileAsync()
{
await _deviceActionService.SelectFileAsync();
}
public void ClearExpirationDate()
{
_isOverridingPickers = true;
ExpirationDate = null;
ExpirationTime = null;
_isOverridingPickers = false;
}
private void UpdateSendData()
{
// filename
if (Send.File != null && FileName != null)
{
Send.File.FileName = FileName;
}
// deletion date
if (ShowDeletionCustomPickers)
{
Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime();
}
else
{
Send.DeletionDate = DeletionDate.ToUniversalTime();
}
// expiration date
if (ExpirationDate.HasValue)
{
if (ShowExpirationCustomPickers && ExpirationTime.HasValue)
{
Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime();
}
else
{
Send.ExpirationDate = ExpirationDate.Value.ToUniversalTime();
}
}
else
{
Send.ExpirationDate = null;
}
}
public async Task<bool> SubmitAsync()
{
if (Send == null)
{
return false;
}
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
if (string.IsNullOrWhiteSpace(Send.Name))
{
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
string.Format(AppResources.ValidationFieldRequired, AppResources.Name),
AppResources.Ok);
return false;
}
if (IsFile)
{
if (!_canAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
return false;
}
if (FileData == null)
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.File),
AppResources.AnErrorHasOccurred);
return false;
}
if (FileData.Length > 104857600) // 100 MB
{
await _platformUtilsService.ShowDialogAsync(AppResources.MaxFileSize,
AppResources.AnErrorHasOccurred);
return false;
}
}
UpdateSendData();
var (send, encryptedFileData) = await _sendService.EncryptAsync(Send, FileData, NewPassword);
if (send == null)
{
return false;
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
var sendId = await _sendService.SaveWithServerAsync(send, encryptedFileData);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null,
EditMode ? AppResources.SendUpdated : AppResources.NewSendCreated);
await Page.Navigation.PopModalAsync();
if (ShareOnSave)
{
var savedSend = await _sendService.GetAsync(sendId);
if (savedSend != null)
{
var savedSendView = await savedSend.DecryptAsync();
await AppHelpers.ShareSendUrl(savedSendView);
}
}
return true;
}
catch (ApiException e)
{
await _deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public async Task<bool> RemovePasswordAsync()
{
return await AppHelpers.RemoveSendPasswordAsync(SendId);
}
public async Task CopyLinkAsync()
{
await _platformUtilsService.CopyToClipboardAsync(AppHelpers.GetSendUrl(Send));
_platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.ShareLink));
}
public async Task ShareLinkAsync()
{
await AppHelpers.ShareSendUrl(Send);
}
public async Task<bool> DeleteAsync()
{
return await AppHelpers.DeleteSendAsync(SendId);
}
private async void TypeChanged()
{
if (Send != null && TypeSelectedIndex > -1)
{
if (!EditMode && TypeOptions[TypeSelectedIndex].Value == SendType.File && !_canAccessPremium)
{
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
TypeSelectedIndex = 0;
}
Send.Type = TypeOptions[TypeSelectedIndex].Value;
TriggerPropertyChanged(nameof(Send), _additionalSendProperties);
}
}
private void DeletionTypeChanged()
{
if (Send != null && DeletionDateTypeSelectedIndex > -1)
{
_isOverridingPickers = true;
switch (DeletionDateTypeSelectedIndex)
{
case 0:
DeletionDate = DateTime.Now.AddHours(1);
break;
case 1:
DeletionDate = DateTime.Now.AddDays(1);
break;
case 2:
DeletionDate = DateTime.Now.AddDays(2);
break;
case 3:
DeletionDate = DateTime.Now.AddDays(3);
break;
case 4:
case 6:
DeletionDate = DateTime.Now.AddDays(7);
break;
case 5:
DeletionDate = DateTime.Now.AddDays(30);
break;
}
DeletionTime = DeletionDate.TimeOfDay;
_isOverridingPickers = false;
TriggerPropertyChanged(nameof(ShowDeletionCustomPickers));
}
}
private void ExpirationTypeChanged()
{
if (Send != null && ExpirationDateTypeSelectedIndex > -1)
{
_isOverridingPickers = true;
switch (ExpirationDateTypeSelectedIndex)
{
case 0:
ClearExpirationDate();
break;
case 1:
ExpirationDate = DateTime.Now.AddHours(1);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 2:
ExpirationDate = DateTime.Now.AddDays(1);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 3:
ExpirationDate = DateTime.Now.AddDays(2);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 4:
ExpirationDate = DateTime.Now.AddDays(3);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 5:
ExpirationDate = DateTime.Now.AddDays(7);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 6:
ExpirationDate = DateTime.Now.AddDays(30);
ExpirationTime = ExpirationDate.Value.TimeOfDay;
break;
case 7:
ClearExpirationDate();
break;
}
_isOverridingPickers = false;
TriggerPropertyChanged(nameof(ShowExpirationCustomPickers));
}
}
private void ExpirationDateChanged()
{
if (!_isOverridingPickers && !ExpirationTime.HasValue)
{
// auto-set time to current time upon setting date
ExpirationTime = DateTime.Now.TimeOfDay;
}
}
private void ExpirationTimeChanged()
{
if (!_isOverridingPickers && !ExpirationDate.HasValue)
{
// auto-set date to current date upon setting time
ExpirationDate = DateTime.Today;
}
}
private void MaxAccessCountChanged()
{
Send.MaxAccessCount = _maxAccessCount;
}
private void TogglePassword()
{
ShowPassword = !ShowPassword;
}
}
}

View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendGroupingsPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:controls="clr-namespace:Bit.App.Controls"
x:DataType="pages:SendGroupingsPageViewModel"
Title="{Binding PageTitle}"
x:Name="_page">
<ContentPage.BindingContext>
<pages:SendGroupingsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Icon="search.png" Clicked="Search_Clicked"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Search}" />
</ContentPage.ToolbarItems>
<ContentPage.Resources>
<ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" />
<ToolbarItem x:Name="_syncItem" x:Key="syncItem" Text="{u:I18n Sync}"
Clicked="Sync_Clicked" Order="Secondary" />
<ToolbarItem x:Name="_lockItem" x:Key="lockItem" Text="{u:I18n Lock}"
Clicked="Lock_Clicked" Order="Secondary" />
<ToolbarItem x:Name="_addItem" x:Key="addItem" Icon="plus.png"
Clicked="AddButton_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n AddItem}" />
<DataTemplate x:Key="sendTemplate"
x:DataType="pages:SendGroupingsPageListItem">
<controls:SendViewCell
Send="{Binding Send}"
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" />
</DataTemplate>
<DataTemplate x:Key="sendGroupTemplate"
x:DataType="pages:SendGroupingsPageListItem">
<ViewCell>
<StackLayout Orientation="Horizontal"
StyleClass="list-row, list-row-platform">
<controls:FaLabel Text="{Binding Icon, Mode=OneWay}"
HorizontalOptions="Start"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform">
<controls:FaLabel.Effects>
<effects:FixedSizeEffect />
</controls:FaLabel.Effects>
</controls:FaLabel>
<Label Text="{Binding Name, Mode=OneWay}"
LineBreakMode="TailTruncation"
HorizontalOptions="FillAndExpand"
VerticalOptions="CenterAndExpand"
StyleClass="list-title" />
<Label Text="{Binding ItemCount, Mode=OneWay}"
HorizontalOptions="End"
VerticalOptions="CenterAndExpand"
HorizontalTextAlignment="End"
StyleClass="list-sub" />
</StackLayout>
</ViewCell>
</DataTemplate>
<pages:SendGroupingsPageListItemSelector x:Key="sendListItemDataTemplateSelector"
SendTemplate="{StaticResource sendTemplate}"
GroupTemplate="{StaticResource sendGroupTemplate}" />
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
<StackLayout
VerticalOptions="CenterAndExpand"
Padding="20, 0"
Spacing="20"
IsVisible="{Binding ShowNoData}">
<Label
Text="{Binding NoDataText}"
HorizontalTextAlignment="Center" />
<Button
Text="{u:I18n AddAnItem}"
Clicked="AddButton_Clicked"
IsVisible="{Binding ShowAddSendButton}" />
</StackLayout>
<controls:ExtendedListView
x:Name="_listView"
IsVisible="{Binding ShowList}"
ItemsSource="{Binding GroupedSends}"
VerticalOptions="FillAndExpand"
HasUnevenRows="True"
RowHeight="-1"
RefreshCommand="{Binding RefreshCommand}"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding Refreshing}"
ItemTemplate="{StaticResource sendListItemDataTemplateSelector}"
IsGroupingEnabled="True"
ItemSelected="RowSelected"
StyleClass="list, list-platform">
<x:Arguments>
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
</x:Arguments>
<ListView.GroupHeaderTemplate>
<DataTemplate x:DataType="pages:SendGroupingsPageListGroup">
<ViewCell>
<StackLayout
Spacing="0" Padding="0" VerticalOptions="FillAndExpand"
StyleClass="list-row-header-container, list-row-header-container-platform">
<BoxView
StyleClass="list-section-separator-top, list-section-separator-top-platform"
IsVisible="{Binding First, Converter={StaticResource inverseBool}}" />
<StackLayout StyleClass="list-row-header, list-row-header-platform">
<Label
Text="{Binding Name}"
StyleClass="list-header, list-header-platform" />
<Label
Text="{Binding ItemCount}"
StyleClass="list-header-sub" />
</StackLayout>
<BoxView
StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
</controls:ExtendedListView>
</StackLayout>
</ResourceDictionary>
</ContentPage.Resources>
<AbsoluteLayout
x:Name="_absLayout"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ContentView
x:Name="_mainContent"
AbsoluteLayout.LayoutFlags="All"
AbsoluteLayout.LayoutBounds="0, 0, 1, 1" />
<Button
x:Name="_fab"
Image="plus.png"
Clicked="AddButton_Clicked"
Style="{StaticResource btn-fab}"
AbsoluteLayout.LayoutFlags="PositionProportional"
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n AddItem}">
<Button.Effects>
<effects:FabShadowEffect />
</Button.Effects>
</Button>
</AbsoluteLayout>
</pages:BaseContentPage>

View file

@ -0,0 +1,188 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Controls;
using Bit.App.Models;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SendGroupingsPage : BaseContentPage
{
private readonly IBroadcasterService _broadcasterService;
private readonly ISyncService _syncService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ISendService _sendService;
private readonly SendGroupingsPageViewModel _vm;
private readonly string _pageName;
private PreviousPageInfo _previousPage;
public SendGroupingsPage(bool mainPage, SendType? type = null, string pageTitle = null,
PreviousPageInfo previousPage = null)
{
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
InitializeComponent();
ListView = _listView;
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
_vm = BindingContext as SendGroupingsPageViewModel;
_vm.Page = this;
_vm.MainPage = mainPage;
_vm.Type = type;
_previousPage = previousPage;
if (pageTitle != null)
{
_vm.PageTitle = pageTitle;
}
if (Device.RuntimePlatform == Device.iOS)
{
_absLayout.Children.Remove(_fab);
ToolbarItems.Add(_addItem);
}
else
{
ToolbarItems.Add(_syncItem);
ToolbarItems.Add(_lockItem);
}
}
public ExtendedListView ListView { get; set; }
protected async override void OnAppearing()
{
base.OnAppearing();
if (_syncService.SyncInProgress)
{
IsBusy = true;
}
_broadcasterService.Subscribe(_pageName, async (message) =>
{
if (message.Command == "syncStarted")
{
Device.BeginInvokeOnMainThread(() => IsBusy = true);
}
else if (message.Command == "syncCompleted")
{
await Task.Delay(500);
Device.BeginInvokeOnMainThread(() =>
{
IsBusy = false;
if (_vm.LoadedOnce)
{
var task = _vm.LoadAsync();
}
});
}
});
await LoadOnAppearedAsync(_mainLayout, false, async () =>
{
if (!_syncService.SyncInProgress || (await _sendService.GetAllAsync()).Any())
{
try
{
await _vm.LoadAsync();
}
catch (Exception e) when (e.Message.Contains("No key."))
{
await Task.Delay(1000);
await _vm.LoadAsync();
}
}
else
{
await Task.Delay(5000);
if (!_vm.Loaded)
{
await _vm.LoadAsync();
}
}
await ShowPreviousPageAsync();
}, _mainContent);
}
protected override void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
}
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
{
((ListView)sender).SelectedItem = null;
if (!DoOnce())
{
return;
}
if (!(e.SelectedItem is SendGroupingsPageListItem item))
{
return;
}
if (item.Send != null)
{
await _vm.SelectSendAsync(item.Send);
}
else if (item.Type != null)
{
await _vm.SelectTypeAsync(item.Type.Value);
}
}
private async void Search_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
var page = new SendsPage(_vm.Filter, _vm.Type != null);
await Navigation.PushModalAsync(new NavigationPage(page), false);
}
}
private async void Sync_Clicked(object sender, EventArgs e)
{
await _vm.SyncAsync();
}
private async void Lock_Clicked(object sender, EventArgs e)
{
await _vaultTimeoutService.LockAsync(true, true);
}
private async void AddButton_Clicked(object sender, EventArgs e)
{
if (DoOnce())
{
var page = new SendAddEditPage(null, _vm.Type);
await Navigation.PushModalAsync(new NavigationPage(page));
}
}
private async Task ShowPreviousPageAsync()
{
if (_previousPage == null)
{
return;
}
if (_previousPage.Page == "view" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
{
await Navigation.PushModalAsync(new NavigationPage(new ViewPage(_previousPage.SendId)));
}
else if (_previousPage.Page == "edit" && !string.IsNullOrWhiteSpace(_previousPage.SendId))
{
await Navigation.PushModalAsync(new NavigationPage(new AddEditPage(_previousPage.SendId)));
}
_previousPage = null;
}
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace Bit.App.Pages
{
public class SendGroupingsPageListGroup : List<SendGroupingsPageListItem>
{
public SendGroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
: this(new List<SendGroupingsPageListItem>(), name, count, doUpper, first) { }
public SendGroupingsPageListGroup(List<SendGroupingsPageListItem> groupItems, string name, int count,
bool doUpper = true, bool first = false)
{
AddRange(groupItems);
if (string.IsNullOrWhiteSpace(name))
{
Name = "-";
}
else if (doUpper)
{
Name = name.ToUpperInvariant();
}
else
{
Name = name;
}
ItemCount = count > 0 ? count.ToString("N0") : "";
First = first;
}
public bool First { get; set; }
public string Name { get; set; }
public string NameShort => string.IsNullOrWhiteSpace(Name) || Name.Length == 0 ? "-" : Name[0].ToString();
public string ItemCount { get; set; }
}
}

View file

@ -0,0 +1,68 @@
using Bit.App.Resources;
using Bit.Core.Enums;
using Bit.Core.Models.View;
namespace Bit.App.Pages
{
public class SendGroupingsPageListItem
{
private string _icon;
private string _name;
public SendView Send { get; set; }
public SendType? Type { get; set; }
public string ItemCount { get; set; }
public string Name
{
get
{
if (_name != null)
{
return _name;
}
if (Type != null)
{
switch (Type.Value)
{
case SendType.Text:
_name = AppResources.TypeText;
break;
case SendType.File:
_name = AppResources.TypeFile;
break;
default:
break;
}
}
return _name;
}
}
public string Icon
{
get
{
if (_icon != null)
{
return _icon;
}
if (Type != null)
{
switch (Type.Value)
{
case SendType.Text:
_icon = "\uf0f6"; // fa-file-text-o
break;
case SendType.File:
_icon = "\uf016"; // fa-file-o
break;
default:
break;
}
}
return _icon;
}
}
}
}

View file

@ -0,0 +1,19 @@
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class SendGroupingsPageListItemSelector : DataTemplateSelector
{
public DataTemplate SendTemplate { get; set; }
public DataTemplate GroupTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if (item is SendGroupingsPageListItem listItem)
{
return listItem.Send != null ? SendTemplate : GroupTemplate;
}
return null;
}
}
}

View file

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Essentials;
using Xamarin.Forms;
using DeviceType = Bit.Core.Enums.DeviceType;
namespace Bit.App.Pages
{
public class SendGroupingsPageViewModel : BaseViewModel
{
private bool _refreshing;
private bool _doingLoad;
private bool _loading;
private bool _loaded;
private bool _showAddSendButton;
private bool _showNoData;
private bool _showList;
private bool _syncRefreshing;
private string _noDataText;
private List<SendView> _allSends;
private Dictionary<SendType, int> _typeCounts = new Dictionary<SendType, int>();
private readonly ISendService _sendService;
private readonly ISyncService _syncService;
private readonly IUserService _userService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IDeviceActionService _deviceActionService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStorageService _storageService;
public SendGroupingsPageViewModel()
{
_sendService = ServiceContainer.Resolve<ISendService>("sendService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
Loading = true;
PageTitle = AppResources.Send;
GroupedSends = new ExtendedObservableCollection<SendGroupingsPageListGroup>();
RefreshCommand = new Command(async () =>
{
Refreshing = true;
await LoadAsync();
});
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
}
public bool MainPage { get; set; }
public SendType? Type { get; set; }
public Func<SendView, bool> Filter { get; set; }
public bool HasSends { get; set; }
public List<SendView> Sends { get; set; }
public bool Refreshing
{
get => _refreshing;
set => SetProperty(ref _refreshing, value);
}
public bool SyncRefreshing
{
get => _syncRefreshing;
set => SetProperty(ref _syncRefreshing, value);
}
public bool Loading
{
get => _loading;
set => SetProperty(ref _loading, value);
}
public bool Loaded
{
get => _loaded;
set => SetProperty(ref _loaded, value);
}
public bool ShowAddSendButton
{
get => _showAddSendButton;
set => SetProperty(ref _showAddSendButton, value);
}
public bool ShowNoData
{
get => _showNoData;
set => SetProperty(ref _showNoData, value);
}
public string NoDataText
{
get => _noDataText;
set => SetProperty(ref _noDataText, value);
}
public bool ShowList
{
get => _showList;
set => SetProperty(ref _showList, value);
}
public ExtendedObservableCollection<SendGroupingsPageListGroup> GroupedSends { get; set; }
public Command RefreshCommand { get; set; }
public Command<SendView> SendOptionsCommand { get; set; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
{
if (_doingLoad)
{
return;
}
var authed = await _userService.IsAuthenticatedAsync();
if (!authed)
{
return;
}
if (await _vaultTimeoutService.IsLockedAsync())
{
return;
}
if (await _storageService.GetAsync<bool>(Constants.SyncOnRefreshKey) && Refreshing && !SyncRefreshing)
{
SyncRefreshing = true;
await _syncService.FullSyncAsync(false);
return;
}
_doingLoad = true;
LoadedOnce = true;
ShowNoData = false;
Loading = true;
ShowList = false;
var groupedSends = new List<SendGroupingsPageListGroup>();
var page = Page as SendGroupingsPage;
try
{
await LoadDataAsync();
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
if (MainPage)
{
groupedSends.Add(new SendGroupingsPageListGroup(
AppResources.Types, 0, uppercaseGroupNames, true)
{
new SendGroupingsPageListItem
{
Type = SendType.Text,
ItemCount = (_typeCounts.ContainsKey(SendType.Text) ?
_typeCounts[SendType.Text] : 0).ToString("N0")
},
new SendGroupingsPageListItem
{
Type = SendType.File,
ItemCount = (_typeCounts.ContainsKey(SendType.File) ?
_typeCounts[SendType.File] : 0).ToString("N0")
},
});
}
if (Sends?.Any() ?? false)
{
var sendsListItems = Sends.Select(s => new SendGroupingsPageListItem { Send = s }).ToList();
groupedSends.Add(new SendGroupingsPageListGroup(sendsListItems,
MainPage ? AppResources.AllSends : AppResources.Sends, sendsListItems.Count,
uppercaseGroupNames, !MainPage));
}
GroupedSends.ResetWithRange(groupedSends);
}
finally
{
_doingLoad = false;
Loaded = true;
Loading = false;
ShowNoData = (MainPage && !HasSends) || !groupedSends.Any();
ShowList = !ShowNoData;
DisableRefreshing();
}
}
public void DisableRefreshing()
{
Refreshing = false;
SyncRefreshing = false;
}
public async Task SelectSendAsync(SendView send)
{
var page = new SendAddEditPage(send.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
public async Task SelectTypeAsync(SendType type)
{
string title = null;
switch (type)
{
case SendType.Text:
title = AppResources.TypeText;
break;
case SendType.File:
title = AppResources.TypeFile;
break;
default:
break;
}
var page = new SendGroupingsPage(false, type, title);
await Page.Navigation.PushAsync(page);
}
public async Task SyncAsync()
{
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return;
}
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
try
{
await _syncService.FullSyncAsync(false, true);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
}
catch
{
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("error", null, AppResources.SyncingFailed);
}
}
private async Task LoadDataAsync()
{
NoDataText = AppResources.NoSends;
_allSends = await _sendService.GetAllDecryptedAsync();
HasSends = _allSends.Any();
_typeCounts.Clear();
Filter = null;
if (MainPage)
{
Sends = _allSends;
foreach (var c in _allSends)
{
if (_typeCounts.ContainsKey(c.Type))
{
_typeCounts[c.Type] = _typeCounts[c.Type] + 1;
}
else
{
_typeCounts.Add(c.Type, 1);
}
}
}
else
{
if (Type != null)
{
Filter = c => c.Type == Type.Value;
}
else
{
PageTitle = AppResources.AllSends;
}
Sends = Filter != null ? _allSends.Where(Filter).ToList() : _allSends;
}
}
private async void SendOptionsAsync(SendView send)
{
if ((Page as BaseContentPage).DoOnce())
{
var selection = await AppHelpers.SendListOptions(Page, send);
if (selection == AppResources.RemovePassword || selection == AppResources.Delete)
{
await LoadAsync();
}
}
}
}
}

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.SendsPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
x:DataType="pages:SendsPageViewModel"
x:Name="_page"
Title="{Binding PageTitle}">
<ContentPage.BindingContext>
<pages:SendsPageViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<u:DateTimeConverter x:Key="dateTime" />
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
x:Name="_closeItem" x:Key="closeItem" />
<StackLayout
Orientation="Horizontal"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"
Spacing="0"
Padding="0"
x:Name="_titleLayout"
x:Key="titleLayout">
<controls:MiButton
StyleClass="btn-title, btn-title-platform"
Text="&#xe5c4;"
VerticalOptions="CenterAndExpand"
Clicked="BackButton_Clicked"
x:Name="_backButton" />
<controls:ExtendedSearchBar
x:Name="_searchBar"
HorizontalOptions="FillAndExpand"
TextChanged="SearchBar_TextChanged"
SearchButtonPressed="SearchBar_SearchButtonPressed"
Placeholder="{Binding PageTitle}" />
</StackLayout>
<BoxView StyleClass="list-section-separator-bottom, list-section-separator-bottom-platform"
x:Name="_separator" x:Key="separator" />
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout x:Name="_mainLayout" Spacing="0" Padding="0">
<controls:FaLabel IsVisible="{Binding ShowSearchDirection}"
Text="&#xf002;"
StyleClass="text-muted"
FontSize="50"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center" />
<Label IsVisible="{Binding ShowNoData}"
Text="{u:I18n NoItemsToList}"
Margin="20, 0"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center" />
<ListView x:Name="_listView"
IsVisible="{Binding ShowList}"
ItemsSource="{Binding Sends}"
VerticalOptions="FillAndExpand"
HasUnevenRows="true"
CachingStrategy="RecycleElement"
ItemSelected="RowSelected"
StyleClass="list, list-platform">
<ListView.ItemTemplate>
<DataTemplate x:DataType="views:SendView">
<controls:SendViewCell
Send="{Binding .}"
ButtonCommand="{Binding BindingContext.SendOptionsCommand, Source={x:Reference _page}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</pages:BaseContentPage>

View file

@ -0,0 +1,109 @@
using System;
using Bit.App.Resources;
using Bit.Core.Models.View;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public partial class SendsPage : BaseContentPage
{
private SendsPageViewModel _vm;
private bool _hasFocused;
public SendsPage(Func<SendView, bool> filter, bool type = false)
{
InitializeComponent();
_vm = BindingContext as SendsPageViewModel;
_vm.Page = this;
_vm.Filter = filter;
if (type)
{
_vm.PageTitle = AppResources.SearchType;
}
else
{
_vm.PageTitle = AppResources.SearchSends;
}
if (Device.RuntimePlatform == Device.iOS)
{
ToolbarItems.Add(_closeItem);
_searchBar.Placeholder = AppResources.Search;
_mainLayout.Children.Insert(0, _searchBar);
_mainLayout.Children.Insert(1, _separator);
}
else
{
NavigationPage.SetTitleView(this, _titleLayout);
}
}
public SearchBar SearchBar => _searchBar;
protected async override void OnAppearing()
{
base.OnAppearing();
await _vm.InitAsync();
if (!_hasFocused)
{
_hasFocused = true;
RequestFocus(_searchBar);
}
}
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
var oldLength = e.OldTextValue?.Length ?? 0;
var newLength = e.NewTextValue?.Length ?? 0;
if (oldLength < 2 && newLength < 2 && oldLength < newLength)
{
return;
}
_vm.Search(e.NewTextValue, 200);
}
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
{
_vm.Search((sender as SearchBar).Text);
}
private void BackButton_Clicked(object sender, EventArgs e)
{
GoBack();
}
protected override bool OnBackButtonPressed()
{
GoBack();
return true;
}
private void GoBack()
{
if (!DoOnce())
{
return;
}
Navigation.PopModalAsync(false);
}
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
{
((ListView)sender).SelectedItem = null;
if (!DoOnce())
{
return;
}
if (e.SelectedItem is SendView send)
{
await _vm.SelectSendAsync(send);
}
}
private void Close_Clicked(object sender, EventArgs e)
{
GoBack();
}
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Pages
{
public class SendsPageViewModel : BaseViewModel
{
private readonly ISearchService _searchService;
private CancellationTokenSource _searchCancellationTokenSource;
private bool _showNoData;
private bool _showList;
public SendsPageViewModel()
{
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
Sends = new ExtendedObservableCollection<SendView>();
SendOptionsCommand = new Command<SendView>(SendOptionsAsync);
}
public Command SendOptionsCommand { get; set; }
public ExtendedObservableCollection<SendView> Sends { get; set; }
public Func<SendView, bool> Filter { get; set; }
public bool ShowNoData
{
get => _showNoData;
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new []
{
nameof(ShowSearchDirection)
});
}
public bool ShowList
{
get => _showList;
set => SetProperty(ref _showList, value, additionalPropertyNames: new []
{
nameof(ShowSearchDirection)
});
}
public bool ShowSearchDirection => !ShowList && !ShowNoData;
public async Task InitAsync()
{
if (!string.IsNullOrWhiteSpace((Page as SendsPage).SearchBar.Text))
{
Search((Page as SendsPage).SearchBar.Text, 200);
}
}
public void Search(string searchText, int? timeout = null)
{
var previousCts = _searchCancellationTokenSource;
var cts = new CancellationTokenSource();
Task.Run(async () =>
{
List<SendView> sends = null;
var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
if (searchable)
{
if (timeout != null)
{
await Task.Delay(timeout.Value);
}
if (searchText != (Page as SendsPage).SearchBar.Text)
{
return;
}
else
{
previousCts?.Cancel();
}
try
{
sends = await _searchService.SearchSendsAsync(searchText, Filter, null, cts.Token);
cts.Token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
return;
}
}
if (sends == null)
{
sends = new List<SendView>();
}
Device.BeginInvokeOnMainThread(() =>
{
Sends.ResetWithRange(sends);
ShowNoData = searchable && Sends.Count == 0;
ShowList = searchable && !ShowNoData;
});
}, cts.Token);
_searchCancellationTokenSource = cts;
}
public async Task SelectSendAsync(SendView send)
{
var page = new SendAddEditPage(send.Id);
await Page.Navigation.PushModalAsync(new NavigationPage(page));
}
private async void SendOptionsAsync(SendView send)
{
if ((Page as BaseContentPage).DoOnce())
{
await AppHelpers.SendListOptions(Page, send);
}
}
}
}

View file

@ -8,6 +8,7 @@ namespace Bit.App.Pages
public class TabsPage : TabbedPage public class TabsPage : TabbedPage
{ {
private NavigationPage _groupingsPage; private NavigationPage _groupingsPage;
private NavigationPage _sendGroupingsPage;
private NavigationPage _generatorPage; private NavigationPage _generatorPage;
public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null) public TabsPage(AppOptions appOptions = null, PreviousPageInfo previousPage = null)
@ -19,6 +20,13 @@ namespace Bit.App.Pages
}; };
Children.Add(_groupingsPage); Children.Add(_groupingsPage);
_sendGroupingsPage = new NavigationPage(new SendGroupingsPage(true))
{
Title = AppResources.Send,
IconImageSource = "paper_plane.png",
};
Children.Add(_sendGroupingsPage);
_generatorPage = new NavigationPage(new GeneratorPage(true, null, this)) _generatorPage = new NavigationPage(new GeneratorPage(true, null, this))
{ {
Title = AppResources.Generator, Title = AppResources.Generator,

View file

@ -3182,5 +3182,263 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("PersonalOwnershipPolicyInEffect", resourceCulture); return ResourceManager.GetString("PersonalOwnershipPolicyInEffect", resourceCulture);
} }
} }
public static string Send {
get {
return ResourceManager.GetString("Send", resourceCulture);
}
}
public static string AllSends {
get {
return ResourceManager.GetString("AllSends", resourceCulture);
}
}
public static string Sends {
get {
return ResourceManager.GetString("Sends", resourceCulture);
}
}
public static string TypeText {
get {
return ResourceManager.GetString("TypeText", resourceCulture);
}
}
public static string HideTextByDefault {
get {
return ResourceManager.GetString("HideTextByDefault", resourceCulture);
}
}
public static string TypeFile {
get {
return ResourceManager.GetString("TypeFile", resourceCulture);
}
}
public static string DeletionDate {
get {
return ResourceManager.GetString("DeletionDate", resourceCulture);
}
}
public static string DeletionTime {
get {
return ResourceManager.GetString("DeletionTime", resourceCulture);
}
}
public static string DeletionDateInfo {
get {
return ResourceManager.GetString("DeletionDateInfo", resourceCulture);
}
}
public static string PendingDelete {
get {
return ResourceManager.GetString("PendingDelete", resourceCulture);
}
}
public static string ExpirationDate {
get {
return ResourceManager.GetString("ExpirationDate", resourceCulture);
}
}
public static string ExpirationTime {
get {
return ResourceManager.GetString("ExpirationTime", resourceCulture);
}
}
public static string ExpirationDateInfo {
get {
return ResourceManager.GetString("ExpirationDateInfo", resourceCulture);
}
}
public static string Expired {
get {
return ResourceManager.GetString("Expired", resourceCulture);
}
}
public static string MaximumAccessCount {
get {
return ResourceManager.GetString("MaximumAccessCount", resourceCulture);
}
}
public static string MaximumAccessCountInfo {
get {
return ResourceManager.GetString("MaximumAccessCountInfo", resourceCulture);
}
}
public static string MaximumAccessCountReached {
get {
return ResourceManager.GetString("MaximumAccessCountReached", resourceCulture);
}
}
public static string CurrentAccessCount {
get {
return ResourceManager.GetString("CurrentAccessCount", resourceCulture);
}
}
public static string NewPassword {
get {
return ResourceManager.GetString("NewPassword", resourceCulture);
}
}
public static string PasswordInfo {
get {
return ResourceManager.GetString("PasswordInfo", resourceCulture);
}
}
public static string RemovePassword {
get {
return ResourceManager.GetString("RemovePassword", resourceCulture);
}
}
public static string AreYouSureRemoveSendPassword {
get {
return ResourceManager.GetString("AreYouSureRemoveSendPassword", resourceCulture);
}
}
public static string RemovingSendPassword {
get {
return ResourceManager.GetString("RemovingSendPassword", resourceCulture);
}
}
public static string SendPasswordRemoved {
get {
return ResourceManager.GetString("SendPasswordRemoved", resourceCulture);
}
}
public static string NotesInfo {
get {
return ResourceManager.GetString("NotesInfo", resourceCulture);
}
}
public static string DisableSend {
get {
return ResourceManager.GetString("DisableSend", resourceCulture);
}
}
public static string NoSends {
get {
return ResourceManager.GetString("NoSends", resourceCulture);
}
}
public static string CopyLink {
get {
return ResourceManager.GetString("CopyLink", resourceCulture);
}
}
public static string ShareLink {
get {
return ResourceManager.GetString("ShareLink", resourceCulture);
}
}
public static string SearchSends {
get {
return ResourceManager.GetString("SearchSends", resourceCulture);
}
}
public static string EditSend {
get {
return ResourceManager.GetString("EditSend", resourceCulture);
}
}
public static string AddSend {
get {
return ResourceManager.GetString("AddSend", resourceCulture);
}
}
public static string AreYouSureDeleteSend {
get {
return ResourceManager.GetString("AreYouSureDeleteSend", resourceCulture);
}
}
public static string SendDeleted {
get {
return ResourceManager.GetString("SendDeleted", resourceCulture);
}
}
public static string SendUpdated {
get {
return ResourceManager.GetString("SendUpdated", resourceCulture);
}
}
public static string NewSendCreated {
get {
return ResourceManager.GetString("NewSendCreated", resourceCulture);
}
}
public static string OneDay {
get {
return ResourceManager.GetString("OneDay", resourceCulture);
}
}
public static string TwoDays {
get {
return ResourceManager.GetString("TwoDays", resourceCulture);
}
}
public static string ThreeDays {
get {
return ResourceManager.GetString("ThreeDays", resourceCulture);
}
}
public static string SevenDays {
get {
return ResourceManager.GetString("SevenDays", resourceCulture);
}
}
public static string ThirtyDays {
get {
return ResourceManager.GetString("ThirtyDays", resourceCulture);
}
}
public static string Custom {
get {
return ResourceManager.GetString("Custom", resourceCulture);
}
}
public static string ShareOnSave {
get {
return ResourceManager.GetString("ShareOnSave", resourceCulture);
}
}
} }
} }

View file

@ -1799,4 +1799,152 @@
<data name="PersonalOwnershipPolicyInEffect" xml:space="preserve"> <data name="PersonalOwnershipPolicyInEffect" xml:space="preserve">
<value>An organization policy is affecting your ownership options.</value> <value>An organization policy is affecting your ownership options.</value>
</data> </data>
<data name="Send" xml:space="preserve">
<value>Send</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AllSends" xml:space="preserve">
<value>All Sends</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Sends" xml:space="preserve">
<value>Sends</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="TypeText" xml:space="preserve">
<value>Text</value>
</data>
<data name="HideTextByDefault" xml:space="preserve">
<value>When accessing the Send, hide the text by default</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="TypeFile" xml:space="preserve">
<value>File</value>
</data>
<data name="DeletionDate" xml:space="preserve">
<value>Deletion Date</value>
</data>
<data name="DeletionTime" xml:space="preserve">
<value>Deletion Time</value>
</data>
<data name="DeletionDateInfo" xml:space="preserve">
<value>The Send will be permanently deleted on the specified date and time.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="PendingDelete" xml:space="preserve">
<value>Pending deletion</value>
</data>
<data name="ExpirationDate" xml:space="preserve">
<value>Expiration Date</value>
</data>
<data name="ExpirationTime" xml:space="preserve">
<value>Expiration Time</value>
</data>
<data name="ExpirationDateInfo" xml:space="preserve">
<value>If set, access to this Send will expire on the specified date and time.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="Expired" xml:space="preserve">
<value>Expired</value>
</data>
<data name="MaximumAccessCount" xml:space="preserve">
<value>Maximum Access Count</value>
</data>
<data name="MaximumAccessCountInfo" xml:space="preserve">
<value>If set, users will no longer be able to access this send once the maximum access count is reached.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="MaximumAccessCountReached" xml:space="preserve">
<value>Max access count reached</value>
</data>
<data name="CurrentAccessCount" xml:space="preserve">
<value>Current Access Count</value>
</data>
<data name="NewPassword" xml:space="preserve">
<value>New Password</value>
</data>
<data name="PasswordInfo" xml:space="preserve">
<value>Optionally require a password for users to access this Send.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="RemovePassword" xml:space="preserve">
<value>Remove Password</value>
</data>
<data name="AreYouSureRemoveSendPassword" xml:space="preserve">
<value>Are you sure you want to remove the password?</value>
</data>
<data name="RemovingSendPassword" xml:space="preserve">
<value>Removing password</value>
</data>
<data name="SendPasswordRemoved" xml:space="preserve">
<value>Password has been removed.</value>
</data>
<data name="NotesInfo" xml:space="preserve">
<value>Private notes about this Send.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="DisableSend" xml:space="preserve">
<value>Disable this Send so that no one can access it.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="NoSends" xml:space="preserve">
<value>There are no sends in your account.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="CopyLink" xml:space="preserve">
<value>Copy Link</value>
</data>
<data name="ShareLink" xml:space="preserve">
<value>Share Link</value>
</data>
<data name="SearchSends" xml:space="preserve">
<value>Search sends</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="EditSend" xml:space="preserve">
<value>Edit Send</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AddSend" xml:space="preserve">
<value>Add Send</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="AreYouSureDeleteSend" xml:space="preserve">
<value>Are you sure you want to delete this Send?</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendDeleted" xml:space="preserve">
<value>Send has been deleted.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="SendUpdated" xml:space="preserve">
<value>Send updated.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="NewSendCreated" xml:space="preserve">
<value>New send created.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
<data name="OneDay" xml:space="preserve">
<value>1 day</value>
</data>
<data name="TwoDays" xml:space="preserve">
<value>2 days</value>
</data>
<data name="ThreeDays" xml:space="preserve">
<value>3 days</value>
</data>
<data name="SevenDays" xml:space="preserve">
<value>7 days</value>
</data>
<data name="ThirtyDays" xml:space="preserve">
<value>30 days</value>
</data>
<data name="Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="ShareOnSave" xml:space="preserve">
<value>Share this Send upon save.</value>
<comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>
</data>
</root> </root>

View file

@ -359,4 +359,11 @@
<Setter Property="Margin" <Setter Property="Margin"
Value="0, -10, 0, 0" /> Value="0, -10, 0, 0" />
</Style> </Style>
<Style TargetType="DatePicker"
Class="datetime-picker">
<Setter Property="FontSize"
Value="Medium" />
<Setter Property="TextColor"
Value="{StaticResource TextColor}" />
</Style>
</ResourceDictionary> </ResourceDictionary>

View file

@ -1,4 +1,5 @@
using Bit.App.Abstractions; using System;
using Bit.App.Abstractions;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core; using Bit.Core;
@ -9,6 +10,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Models; using Bit.App.Models;
using Bit.Core.Exceptions;
using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Utilities namespace Bit.App.Utilities
@ -130,6 +133,143 @@ namespace Bit.App.Utilities
return selection; return selection;
} }
public static async Task<string> SendListOptions(ContentPage page, SendView send)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
var options = new List<string> { AppResources.Edit };
options.Add(AppResources.CopyLink);
options.Add(AppResources.ShareLink);
if (send.HasPassword)
{
options.Add(AppResources.RemovePassword);
}
var selection = await page.DisplayActionSheet(send.Name, AppResources.Cancel, AppResources.Delete,
options.ToArray());
if (await vaultTimeoutService.IsLockedAsync())
{
platformUtilsService.ShowToast("info", null, AppResources.VaultIsLocked);
}
else if (selection == AppResources.Edit)
{
await page.Navigation.PushModalAsync(new NavigationPage(new SendAddEditPage(send.Id)));
}
else if (selection == AppResources.CopyLink)
{
await platformUtilsService.CopyToClipboardAsync(GetSendUrl(send));
platformUtilsService.ShowToast("info", null,
string.Format(AppResources.ValueHasBeenCopied, AppResources.ShareLink));
}
else if (selection == AppResources.ShareLink)
{
await ShareSendUrl(send);
}
else if (selection == AppResources.RemovePassword)
{
await RemoveSendPasswordAsync(send.Id);
}
else if (selection == AppResources.Delete)
{
await DeleteSendAsync(send.Id);
}
return selection;
}
public static string GetSendUrl(SendView send)
{
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
return environmentService.BaseUrl + "/#/send/" + send.AccessId + "/" + send.UrlB64Key;
}
public static async Task ShareSendUrl(SendView send)
{
await Share.RequestAsync(new ShareTextRequest
{
Uri = new Uri(GetSendUrl(send)).ToString(),
Title = AppResources.Send + " " + send.Name,
Subject = send.Name
});
}
public static async Task<bool> RemoveSendPasswordAsync(string sendId)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await platformUtilsService.ShowDialogAsync(
AppResources.AreYouSureRemoveSendPassword,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return false;
}
try
{
await deviceActionService.ShowLoadingAsync(AppResources.RemovingSendPassword);
await sendService.RemovePasswordWithServerAsync(sendId);
await deviceActionService.HideLoadingAsync();
platformUtilsService.ShowToast("success", null, AppResources.SendPasswordRemoved);
return true;
}
catch (ApiException e)
{
await deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public static async Task<bool> DeleteSendAsync(string sendId)
{
var platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
if (Connectivity.NetworkAccess == NetworkAccess.None)
{
await platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
AppResources.InternetConnectionRequiredTitle);
return false;
}
var confirmed = await platformUtilsService.ShowDialogAsync(
AppResources.AreYouSureDeleteSend,
null, AppResources.Yes, AppResources.Cancel);
if (!confirmed)
{
return false;
}
try
{
await deviceActionService.ShowLoadingAsync(AppResources.Deleting);
await sendService.DeleteWithServerAsync(sendId);
await deviceActionService.HideLoadingAsync();
platformUtilsService.ShowToast("success", null, AppResources.SendDeleted);
return true;
}
catch (ApiException e)
{
await deviceActionService.HideLoadingAsync();
if (e?.Error != null)
{
await platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
AppResources.AnErrorHasOccurred);
}
}
return false;
}
public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService, public static async Task<bool> PerformUpdateTasksAsync(ISyncService syncService,
IDeviceActionService deviceActionService, IStorageService storageService) IDeviceActionService deviceActionService, IStorageService storageService)
{ {

View file

@ -15,5 +15,9 @@ namespace Bit.Core.Abstractions
List<CipherView> ciphers = null, CancellationToken ct = default); List<CipherView> ciphers = null, CancellationToken ct = default);
List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query, List<CipherView> SearchCiphersBasic(List<CipherView> ciphers, string query,
CancellationToken ct = default, bool deleted = false); CancellationToken ct = default, bool deleted = false);
Task<List<SendView>> SearchSendsAsync(string query, Func<SendView, bool> filter = null,
List<SendView> sends = null, CancellationToken ct = default);
List<SendView> SearchSendsBasic(List<SendView> sends, string query,
CancellationToken ct = default, bool deleted = false);
} }
} }

View file

@ -9,12 +9,12 @@ namespace Bit.Core.Abstractions
public interface ISendService public interface ISendService
{ {
void ClearCache(); void ClearCache();
Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password, Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, string password,
SymmetricCryptoKey key = null); SymmetricCryptoKey key = null);
Task<Send> GetAsync(string id); Task<Send> GetAsync(string id);
Task<List<Send>> GetAllAsync(); Task<List<Send>> GetAllAsync();
Task<List<SendView>> GetAllDecryptedAsync(); Task<List<SendView>> GetAllDecryptedAsync();
Task SaveWithServerAsync(Send sendData, byte[] encryptedFileData); Task<string> SaveWithServerAsync(Send sendData, byte[] encryptedFileData);
Task UpsertAsync(params SendData[] send); Task UpsertAsync(params SendData[] send);
Task ReplaceAsync(Dictionary<string, SendData> sends); Task ReplaceAsync(Dictionary<string, SendData> sends);
Task ClearAsync(string userId); Task ClearAsync(string userId);

View file

@ -15,10 +15,10 @@ namespace Bit.Core.Models.Response
public SendTextApi Text { get; set; } public SendTextApi Text { get; set; }
public string Key { get; set; } public string Key { get; set; }
public int? MaxAccessCount { get; set; } public int? MaxAccessCount { get; set; }
public int AccessCount { get; internal set; } public int AccessCount { get; set; }
public DateTime RevisionDate { get; internal set; } public DateTime RevisionDate { get; set; }
public DateTime? ExpirationDate { get; internal set; } public DateTime? ExpirationDate { get; set; }
public DateTime DeletionDate { get; internal set; } public DateTime DeletionDate { get; set; }
public string Password { get; set; } public string Password { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }
} }

View file

@ -7,6 +7,8 @@ namespace Bit.Core.Models.View
{ {
public class SendView : View public class SendView : View
{ {
public SendView() { }
public SendView(Send send) : base() public SendView(Send send) : base()
{ {
Id = send.Id; Id = send.Id;
@ -38,8 +40,10 @@ namespace Bit.Core.Models.View
public string Password { get; set; } public string Password { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }
public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key); public string UrlB64Key => Key == null ? null : CoreHelpers.Base64UrlEncode(Key);
public bool HasPassword => Password?.Length > 0;
public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value; public bool MaxAccessCountReached => MaxAccessCount.HasValue && AccessCount >= MaxAccessCount.Value;
public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow; public bool Expired => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;
public bool PendingDelete => DeletionDate <= DateTime.UtcNow; public bool PendingDelete => DeletionDate <= DateTime.UtcNow;
public string DisplayDate => DeletionDate.ToLocalTime().ToString("MMM d, yyyy, h:mm tt");
} }
} }

View file

@ -230,7 +230,7 @@ namespace Bit.Core.Services
SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true); SendAsync<SendRequest, SendResponse>(HttpMethod.Put, $"/sends/{id}", request, true, true);
public Task<SendResponse> PutSendRemovePasswordAsync(string id) => public Task<SendResponse> PutSendRemovePasswordAsync(string id) =>
SendAsync<object, SendResponse>(HttpMethod.Put, $"/sends/{id}", null, true, true); SendAsync<object, SendResponse>(HttpMethod.Put, $"/sends/{id}/remove-password", null, true, true);
public Task DeleteSendAsync(string id) => public Task DeleteSendAsync(string id) =>
SendAsync<object, object>(HttpMethod.Delete, $"/sends/{id}", null, true, false); SendAsync<object, object>(HttpMethod.Delete, $"/sends/{id}", null, true, false);

View file

@ -429,7 +429,7 @@ namespace Bit.Core.Services
public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial) public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial)
{ {
var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 65, HkdfAlgorithm.Sha256); var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 64, HkdfAlgorithm.Sha256);
return new SymmetricCryptoKey(sendKey); return new SymmetricCryptoKey(sendKey);
} }

View file

@ -11,11 +11,14 @@ namespace Bit.Core.Services
public class SearchService : ISearchService public class SearchService : ISearchService
{ {
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly ISendService _sendService;
public SearchService( public SearchService(
ICipherService cipherService) ICipherService cipherService,
ISendService sendService)
{ {
_cipherService = cipherService; _cipherService = cipherService;
_sendService = sendService;
} }
public void ClearIndex() public void ClearIndex()
@ -94,5 +97,61 @@ namespace Bit.Core.Services
return false; return false;
}).ToList(); }).ToList();
} }
public async Task<List<SendView>> SearchSendsAsync(string query, Func<SendView, bool> filter = null,
List<SendView> sends = null, CancellationToken ct = default)
{
var results = new List<SendView>();
if (query != null)
{
query = query.Trim().ToLower();
}
if (query == string.Empty)
{
query = null;
}
if (sends == null)
{
sends = await _sendService.GetAllDecryptedAsync();
}
ct.ThrowIfCancellationRequested();
if (filter != null)
{
sends = sends.Where(filter).ToList();
}
ct.ThrowIfCancellationRequested();
if (!IsSearchable(query))
{
return sends;
}
return SearchSendsBasic(sends, query);
}
public List<SendView> SearchSendsBasic(List<SendView> sends, string query, CancellationToken ct = default,
bool deleted = false)
{
ct.ThrowIfCancellationRequested();
query = query.Trim().ToLower();
return sends.Where(s =>
{
ct.ThrowIfCancellationRequested();
if (s.Name?.ToLower().Contains(query) ?? false)
{
return true;
}
if (s.Text?.Text?.ToLower().Contains(query) ?? false)
{
return true;
}
if (s.File?.FileName?.ToLower()?.Contains(query) ?? false)
{
return true;
}
return false;
}).ToList();
}
} }
} }

View file

@ -77,7 +77,7 @@ namespace Bit.Core.Services
await DeleteAsync(id); await DeleteAsync(id);
} }
public async Task<(Send send, CipherString encryptedFileData)> EncryptAsync(SendView model, byte[] fileData, public async Task<(Send send, byte[] encryptedFileData)> EncryptAsync(SendView model, byte[] fileData,
string password, SymmetricCryptoKey key = null) string password, SymmetricCryptoKey key = null)
{ {
if (model.Key == null) if (model.Key == null)
@ -91,17 +91,20 @@ namespace Bit.Core.Services
Id = model.Id, Id = model.Id,
Type = model.Type, Type = model.Type,
Disabled = model.Disabled, Disabled = model.Disabled,
DeletionDate = model.DeletionDate,
ExpirationDate = model.ExpirationDate,
MaxAccessCount = model.MaxAccessCount, MaxAccessCount = model.MaxAccessCount,
Key = await _cryptoService.EncryptAsync(model.Key, key), Key = await _cryptoService.EncryptAsync(model.Key, key),
Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey), Name = await _cryptoService.EncryptAsync(model.Name, model.CryptoKey),
Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey), Notes = await _cryptoService.EncryptAsync(model.Notes, model.CryptoKey),
}; };
CipherString encryptedFileData = null; byte[] encryptedFileData = null;
if (password != null) if (password != null)
{ {
var kdfIterations = await _userService.GetKdfIterationsAsync() ?? 100000;
var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key, var passwordHash = await _cryptoFunctionService.Pbkdf2Async(password, model.Key,
CryptoHashAlgorithm.Sha256, 100000); CryptoHashAlgorithm.Sha256, kdfIterations);
send.Password = Convert.ToBase64String(passwordHash); send.Password = Convert.ToBase64String(passwordHash);
} }
@ -119,7 +122,7 @@ namespace Bit.Core.Services
if (fileData != null) if (fileData != null)
{ {
send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey); send.File.FileName = await _cryptoService.EncryptAsync(model.File.FileName, model.CryptoKey);
encryptedFileData = await _cryptoService.EncryptAsync(fileData, model.CryptoKey); encryptedFileData = await _cryptoService.EncryptToBytesAsync(fileData, model.CryptoKey);
} }
break; break;
default: default:
@ -133,7 +136,7 @@ namespace Bit.Core.Services
{ {
var userId = await _userService.GetUserIdAsync(); var userId = await _userService.GetUserIdAsync();
var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)); var sends = await _storageService.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId));
return sends.Select(kvp => new Send(kvp.Value)).ToList(); return sends?.Select(kvp => new Send(kvp.Value)).ToList() ?? new List<Send>();
} }
public async Task<List<SendView>> GetAllDecryptedAsync() public async Task<List<SendView>> GetAllDecryptedAsync()
@ -161,7 +164,7 @@ namespace Bit.Core.Services
async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync()); async Task decryptAndAddSendAsync(Send send) => decSends.Add(await send.DecryptAsync());
await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s))); await Task.WhenAll((await GetAllAsync()).Select(s => decryptAndAddSendAsync(s)));
decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList(); decSends = decSends.OrderBy(s => s, new SendLocaleComparer(_i18nService)).ToList();
_decryptedSendsCache = decSends; _decryptedSendsCache = decSends;
return _decryptedSendsCache; return _decryptedSendsCache;
} }
@ -190,9 +193,8 @@ namespace Bit.Core.Services
_decryptedSendsCache = null; _decryptedSendsCache = null;
} }
public async Task SaveWithServerAsync(Send send, byte[] encryptedFileData) public async Task<string> SaveWithServerAsync(Send send, byte[] encryptedFileData)
{ {
var request = new SendRequest(send); var request = new SendRequest(send);
SendResponse response; SendResponse response;
if (send.Id == null) if (send.Id == null)
@ -223,6 +225,7 @@ namespace Bit.Core.Services
var userId = await _userService.GetUserIdAsync(); var userId = await _userService.GetUserIdAsync();
await UpsertAsync(new SendData(response, userId)); await UpsertAsync(new SendData(response, userId));
return response.Id;
} }
public async Task UpsertAsync(params SendData[] sends) public async Task UpsertAsync(params SendData[] sends)

View file

@ -374,7 +374,11 @@ namespace Bit.Core.Services
await _policyService.Replace(policies); await _policyService.Replace(policies);
} }
private Task SyncSendsAsync(string userId, List<SendResponse> sends) => private async Task SyncSendsAsync(string userId, List<SendResponse> response)
_sendService.ReplaceAsync(sends.ToDictionary(s => userId, s => new SendData(s, userId))); {
var sends = response?.ToDictionary(s => s.Id, s => new SendData(s, userId)) ??
new Dictionary<string, SendData>();
await _sendService.ReplaceAsync(sends);
}
} }
} }

View file

@ -45,7 +45,9 @@ namespace Bit.Core.Utilities
var folderService = new FolderService(cryptoService, userService, apiService, storageService, var folderService = new FolderService(cryptoService, userService, apiService, storageService,
i18nService, cipherService); i18nService, cipherService);
var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService); var collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
searchService = new SearchService(cipherService); var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
cryptoFunctionService);
searchService = new SearchService(cipherService, sendService);
var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService, var vaultTimeoutService = new VaultTimeoutService(cryptoService, userService, platformUtilsService,
storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService, storageService, folderService, cipherService, collectionService, searchService, messagingService, tokenService,
null, (expired) => null, (expired) =>
@ -54,8 +56,6 @@ namespace Bit.Core.Utilities
return Task.FromResult(0); return Task.FromResult(0);
}); });
var policyService = new PolicyService(storageService, userService); var policyService = new PolicyService(storageService, userService);
var sendService = new SendService(cryptoService, userService, apiService, storageService, i18nService,
cryptoFunctionService);
var syncService = new SyncService(userService, apiService, settingsService, folderService, var syncService = new SyncService(userService, apiService, settingsService, folderService,
cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService, sendService,
(bool expired) => (bool expired) =>
@ -84,6 +84,7 @@ namespace Bit.Core.Utilities
Register<ICipherService>("cipherService", cipherService); Register<ICipherService>("cipherService", cipherService);
Register<IFolderService>("folderService", folderService); Register<IFolderService>("folderService", folderService);
Register<ICollectionService>("collectionService", collectionService); Register<ICollectionService>("collectionService", collectionService);
Register<ISendService>("sendService", sendService);
Register<ISearchService>("searchService", searchService); Register<ISearchService>("searchService", searchService);
Register<IPolicyService>("policyService", policyService); Register<IPolicyService>("policyService", policyService);
Register<ISyncService>("syncService", syncService); Register<ISyncService>("syncService", syncService);

View file

@ -0,0 +1,59 @@
using System.ComponentModel;
using Bit.App.Controls;
using Bit.iOS.Core.Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtendedDatePicker), typeof(ExtendedDatePickerRenderer))]
namespace Bit.iOS.Core.Renderers
{
public class ExtendedDatePickerRenderer : DatePickerRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
{
base.OnElementChanged(e);
if (Control != null && Element is ExtendedDatePicker element)
{
// center text
Control.TextAlignment = UITextAlignment.Center;
// use placeholder until NullableDate set
if (!element.NullableDate.HasValue)
{
Control.Text = element.PlaceHolder;
}
// force use of wheel picker on iOS 14+
// TODO remove this when we upgrade to X.F 5 SR-1
// https://github.com/xamarin/Xamarin.Forms/issues/12258#issuecomment-700168665
try
{
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 2))
{
var picker = (UIDatePicker)Control.InputView;
picker.PreferredDatePickerStyle = UIDatePickerStyle.Wheels;
}
}
catch { }
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == DatePicker.DateProperty.PropertyName ||
e.PropertyName == DatePicker.FormatProperty.PropertyName)
{
if (Control != null && Element is ExtendedDatePicker element)
{
if (Element.Format == element.PlaceHolder)
{
Control.Text = element.PlaceHolder;
return;
}
}
}
base.OnElementPropertyChanged(sender, e);
}
}
}

View file

@ -0,0 +1,59 @@
using System.ComponentModel;
using Bit.App.Controls;
using Bit.iOS.Core.Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtendedTimePicker), typeof(ExtendedTimePickerRenderer))]
namespace Bit.iOS.Core.Renderers
{
public class ExtendedTimePickerRenderer : TimePickerRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
{
base.OnElementChanged(e);
if (Control != null && Element is ExtendedTimePicker element)
{
// center text
Control.TextAlignment = UITextAlignment.Center;
// use placeholder until NullableTime set
if (!element.NullableTime.HasValue)
{
Control.Text = element.PlaceHolder;
}
// force use of wheel picker on iOS 14+
// TODO remove this when we upgrade to X.F 5 SR-1
// https://github.com/xamarin/Xamarin.Forms/issues/12258#issuecomment-700168665
try
{
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 2))
{
var picker = (UIDatePicker)Control.InputView;
picker.PreferredDatePickerStyle = UIDatePickerStyle.Wheels;
}
}
catch { }
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName ||
e.PropertyName == TimePicker.FormatProperty.PropertyName)
{
if (Control != null && Element is ExtendedTimePicker element)
{
if (Element.Format == element.PlaceHolder)
{
Control.Text = element.PlaceHolder;
return;
}
}
}
base.OnElementPropertyChanged(sender, e);
}
}
}

View file

@ -155,12 +155,14 @@
<Compile Include="NFCReaderDelegate.cs" /> <Compile Include="NFCReaderDelegate.cs" />
<Compile Include="Renderers\CustomButtonRenderer.cs" /> <Compile Include="Renderers\CustomButtonRenderer.cs" />
<Compile Include="Renderers\CustomContentPageRenderer.cs" /> <Compile Include="Renderers\CustomContentPageRenderer.cs" />
<Compile Include="Renderers\ExtendedDatePickerRenderer.cs" />
<Compile Include="Renderers\CustomEditorRenderer.cs" /> <Compile Include="Renderers\CustomEditorRenderer.cs" />
<Compile Include="Renderers\CustomEntryRenderer.cs" /> <Compile Include="Renderers\CustomEntryRenderer.cs" />
<Compile Include="Renderers\CustomLabelRenderer.cs" /> <Compile Include="Renderers\CustomLabelRenderer.cs" />
<Compile Include="Renderers\CustomPickerRenderer.cs" /> <Compile Include="Renderers\CustomPickerRenderer.cs" />
<Compile Include="Renderers\CustomSearchBarRenderer.cs" /> <Compile Include="Renderers\CustomSearchBarRenderer.cs" />
<Compile Include="Renderers\CustomTabbedRenderer.cs" /> <Compile Include="Renderers\CustomTabbedRenderer.cs" />
<Compile Include="Renderers\ExtendedTimePickerRenderer.cs" />
<Compile Include="Renderers\CustomViewCellRenderer.cs" /> <Compile Include="Renderers\CustomViewCellRenderer.cs" />
<Compile Include="Renderers\HybridWebViewRenderer.cs" /> <Compile Include="Renderers\HybridWebViewRenderer.cs" />
<Compile Include="Services\BiometricService.cs" /> <Compile Include="Services\BiometricService.cs" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -267,6 +267,15 @@
<ItemGroup> <ItemGroup>
<BundleResource Include="Resources\logo%403x.png" /> <BundleResource Include="Resources\logo%403x.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\paper_plane.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\paper_plane%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\paper_plane%403x.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<BundleResource Include="Resources\plus.png" /> <BundleResource Include="Resources\plus.png" />
</ItemGroup> </ItemGroup>

View file

@ -339,6 +339,8 @@ namespace Bit.Core.Test.Services
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}"); new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
CipherString encrypt(string secret, SymmetricCryptoKey key) => CipherString encrypt(string secret, SymmetricCryptoKey key) =>
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}"); new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
byte[] encryptFileBytes(byte[] secret, SymmetricCryptoKey key) =>
secret.Concat(key.Key).ToArray();
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>()) sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
.Returns(info => getPbkdf((string)info[0], (byte[])info[1])); .Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
@ -346,12 +348,14 @@ namespace Bit.Core.Test.Services
.Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1])); .Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>()) sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1])); .Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1]));
sutProvider.GetDependency<ICryptoService>().EncryptToBytesAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => encryptFileBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey); var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey);
TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File", TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File",
"AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key", "AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key",
"MaxAccessCountReached", "Expired", "PendingDelete"); "MaxAccessCountReached", "Expired", "PendingDelete", "HasPassword", "DisplayDate");
Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password); Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password);
TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key); TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key);
TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name); TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name);
@ -366,7 +370,7 @@ namespace Bit.Core.Test.Services
case SendType.File: case SendType.File:
// Only set filename // Only set filename
TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName); TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName);
TestHelper.AssertPropertyEqual(encryptBytes(fileData, view.CryptoKey), encryptedFileData); Assert.Equal(encryptFileBytes(fileData, view.CryptoKey), encryptedFileData);
break; break;
default: default:
throw new Exception("Untested send type"); throw new Exception("Untested send type");