diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..a9f9d67495ca6f98f5a611842ae41f2f86353448
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "Ring/WhirlyGlobeMaply"]
+	path = Ring/WhirlyGlobeMaply
+	url = https://github.com/mousebird/WhirlyGlobe.git
diff --git a/Ring/.swiftlint.yml b/Ring/.swiftlint.yml
index d3b6db03e794067b6d068bfe68acc9cd4587f25f..4d1bf6a29bb73154f33b75224e82921a83825bcd 100644
--- a/Ring/.swiftlint.yml
+++ b/Ring/.swiftlint.yml
@@ -8,6 +8,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
   - Carthage
   - Pods
   - Ring/Constants
+  - WhirlyGlobeMaply
 
 force_cast: warning # implicitly
 force_try:
diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index d2ce32b16bb5cc01c89193f7ca67dae38929e977..cb1aebf192e2244b137ed7b410fd92e0300cbb1a 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -314,6 +314,18 @@
 		62DFAB2E1F9FF0D0002D6F9C /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DFAB2D1F9FF0D0002D6F9C /* NetworkService.swift */; };
 		62E55B6D1F758E6F00D3FEF4 /* String+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E55B6C1F758E6F00D3FEF4 /* String+Helpers.swift */; };
 		62E55B6F1F793ADE00D3FEF4 /* AvatarsColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E55B6E1F793ADE00D3FEF4 /* AvatarsColors.swift */; };
+		6452144424B4ACA7007203D5 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144324B4ACA7007203D5 /* CoreLocation.framework */; };
+		645BDD6224B5FEFE009129B1 /* WhirlyGlobeMaplyComponent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452143D24B4AB44007203D5 /* WhirlyGlobeMaplyComponent.framework */; };
+		645BDD6324B5FEFF009129B1 /* WhirlyGlobeMaplyComponent.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6452143D24B4AB44007203D5 /* WhirlyGlobeMaplyComponent.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		645BDD7724B7415A009129B1 /* MessageCellLocationSharingSent.xib in Resources */ = {isa = PBXBuildFile; fileRef = 645BDD6C24B7415A009129B1 /* MessageCellLocationSharingSent.xib */; };
+		645BDD7B24B7415A009129B1 /* MessageCellLocationSharingSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645BDD7024B7415A009129B1 /* MessageCellLocationSharingSent.swift */; };
+		645BDD8124B74BCB009129B1 /* LocationSharingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645BDD8024B74BCB009129B1 /* LocationSharingService.swift */; };
+		649AD3C324B4CFC700A0236D /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144724B4ACDE007203D5 /* libsqlite3.tbd */; };
+		649AD3C624B4CFD500A0236D /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144524B4ACC8007203D5 /* libxml2.tbd */; };
+		649AD3C724B4D00100A0236D /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6452144124B4AC9F007203D5 /* libc++.tbd */; };
+		64F8127724B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F8127324B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift */; };
+		64F8127824B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib in Resources */ = {isa = PBXBuildFile; fileRef = 64F8127624B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib */; };
+		64F8127A24BBC19C00A7DE6A /* MessageCellLocationSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F8127924BBC19C00A7DE6A /* MessageCellLocationSharing.swift */; };
 		6613A612214AFF4700B497D1 /* ScanViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6613A611214AFF4700B497D1 /* ScanViewController.storyboard */; };
 		66266FC021557D2F002757A6 /* ScanViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66266FBF21557D2F002757A6 /* ScanViewModel.swift */; };
 		66266FC4215C18F8002757A6 /* Emoji+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66266FC3215C18F8002757A6 /* Emoji+Helpers.swift */; };
@@ -338,8 +350,36 @@
 			remoteGlobalIDString = 043999F21D1C2D9D00E99CD9;
 			remoteInfo = Ring;
 		};
+		6452143C24B4AB44007203D5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 6452143724B4AB43007203D5 /* WhirlyGlobeMaplyComponent.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 2BE536FF1D2499E500B60FAD;
+			remoteInfo = WhirlyGlobeMaplyComponent;
+		};
+		6452143E24B4AB44007203D5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 6452143724B4AB43007203D5 /* WhirlyGlobeMaplyComponent.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 2BE537091D2499E500B60FAD;
+			remoteInfo = WhirlyGlobeMaplyComponentTests;
+		};
 /* End PBXContainerItemProxy section */
 
+/* Begin PBXCopyFilesBuildPhase section */
+		645BDD6424B5FEFF009129B1 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				645BDD6324B5FEFF009129B1 /* WhirlyGlobeMaplyComponent.framework in Embed Frameworks */,
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
 /* Begin PBXFileReference section */
 		024B612B1DF7654F00C4F9DE /* DaemonServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DaemonServiceTests.swift; sourceTree = "<group>"; };
 		024B612D1DF7656A00C4F9DE /* FixtureFailInitDRingAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FixtureFailInitDRingAdapter.h; path = Fixtures/DRingAdaptor/FixtureFailInitDRingAdapter.h; sourceTree = "<group>"; };
@@ -727,6 +767,17 @@
 		62DFAB2D1F9FF0D0002D6F9C /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
 		62E55B6C1F758E6F00D3FEF4 /* String+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Helpers.swift"; sourceTree = "<group>"; };
 		62E55B6E1F793ADE00D3FEF4 /* AvatarsColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarsColors.swift; sourceTree = "<group>"; };
+		6452143724B4AB43007203D5 /* WhirlyGlobeMaplyComponent.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = WhirlyGlobeMaplyComponent.xcodeproj; path = "WhirlyGlobeMaply/ios/library/WhirlyGlobe-MaplyComponent/WhirlyGlobeMaplyComponent.xcodeproj"; sourceTree = "<group>"; };
+		6452144124B4AC9F007203D5 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
+		6452144324B4ACA7007203D5 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
+		6452144524B4ACC8007203D5 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
+		6452144724B4ACDE007203D5 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
+		645BDD6C24B7415A009129B1 /* MessageCellLocationSharingSent.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellLocationSharingSent.xib; sourceTree = "<group>"; };
+		645BDD7024B7415A009129B1 /* MessageCellLocationSharingSent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharingSent.swift; sourceTree = "<group>"; };
+		645BDD8024B74BCB009129B1 /* LocationSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingService.swift; sourceTree = "<group>"; };
+		64F8127324B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharingReceived.swift; sourceTree = "<group>"; };
+		64F8127624B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellLocationSharingReceived.xib; sourceTree = "<group>"; };
+		64F8127924BBC19C00A7DE6A /* MessageCellLocationSharing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLocationSharing.swift; sourceTree = "<group>"; };
 		6613A611214AFF4700B497D1 /* ScanViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ScanViewController.storyboard; sourceTree = "<group>"; };
 		66266FBF21557D2F002757A6 /* ScanViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewModel.swift; sourceTree = "<group>"; };
 		66266FC3215C18F8002757A6 /* Emoji+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Emoji+Helpers.swift"; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -741,6 +792,11 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				649AD3C324B4CFC700A0236D /* libsqlite3.tbd in Frameworks */,
+				649AD3C624B4CFD500A0236D /* libxml2.tbd in Frameworks */,
+				04399A981D1C2F6100E99CD9 /* libz.tbd in Frameworks */,
+				649AD3C724B4D00100A0236D /* libc++.tbd in Frameworks */,
+				6452144424B4ACA7007203D5 /* CoreLocation.framework in Frameworks */,
 				0E4A51CC23282BCC00357AFC /* libhttp_parser.a in Frameworks */,
 				0ECB4E2A22B2D4BB0097CD7B /* CallKit.framework in Frameworks */,
 				0ECEE9A3220D1935000E1CF4 /* VideoToolbox.framework in Frameworks */,
@@ -801,7 +857,7 @@
 				04399B141D1C341A00E99CD9 /* libx264.a in Frameworks */,
 				04399B151D1C341A00E99CD9 /* libyaml-cpp.a in Frameworks */,
 				04399A971D1C2F6100E99CD9 /* libbz2.tbd in Frameworks */,
-				04399A981D1C2F6100E99CD9 /* libz.tbd in Frameworks */,
+				645BDD6224B5FEFE009129B1 /* WhirlyGlobeMaplyComponent.framework in Frameworks */,
 				04399A941D1C2F5800E99CD9 /* libiconv.tbd in Frameworks */,
 				04399A2C1D1C2DE900E99CD9 /* AVFoundation.framework in Frameworks */,
 				04399A2A1D1C2DE300E99CD9 /* CoreMedia.framework in Frameworks */,
@@ -847,6 +903,11 @@
 		02AED8171DD4C4B000F740BA /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				6452144724B4ACDE007203D5 /* libsqlite3.tbd */,
+				6452144524B4ACC8007203D5 /* libxml2.tbd */,
+				6452144324B4ACA7007203D5 /* CoreLocation.framework */,
+				6452144124B4AC9F007203D5 /* libc++.tbd */,
+				6452143724B4AB43007203D5 /* WhirlyGlobeMaplyComponent.xcodeproj */,
 				0E4A51CB23282BCC00357AFC /* libhttp_parser.a */,
 				0ECB4E2922B2D4BB0097CD7B /* CallKit.framework */,
 				0E639459224AB32200C0890A /* Contacts.framework */,
@@ -907,6 +968,7 @@
 				62B60AF320489E7C001BEACF /* DataTransferService.swift */,
 				62B60AFA2048A437001BEACF /* DataTransferAdapterDelegate.swift */,
 				0ECB4E2722B2D4840097CD7B /* CallsProviderDelegate.swift */,
+				645BDD8024B74BCB009129B1 /* LocationSharingService.swift */,
 			);
 			path = Services;
 			sourceTree = "<group>";
@@ -1524,6 +1586,11 @@
 				62AD58472056DA6800AF0701 /* MessageCellDataTransferReceived.xib */,
 				62AD58492056DADF00AF0701 /* MessageCellDataTransferReceived.swift */,
 				62AD584B2056DB2700AF0701 /* MessageCellDataTransferSent.swift */,
+				64F8127924BBC19C00A7DE6A /* MessageCellLocationSharing.swift */,
+				645BDD7024B7415A009129B1 /* MessageCellLocationSharingSent.swift */,
+				645BDD6C24B7415A009129B1 /* MessageCellLocationSharingSent.xib */,
+				64F8127324B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift */,
+				64F8127624B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib */,
 			);
 			path = Cells;
 			sourceTree = "<group>";
@@ -1678,6 +1745,15 @@
 			name = Cells;
 			sourceTree = "<group>";
 		};
+		6452143824B4AB43007203D5 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				6452143D24B4AB44007203D5 /* WhirlyGlobeMaplyComponent.framework */,
+				6452143F24B4AB44007203D5 /* WhirlyGlobeMaplyComponentTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
 		6613A610214AF8B100B497D1 /* QRCode */ = {
 			isa = PBXGroup;
 			children = (
@@ -1701,6 +1777,7 @@
 				043999EF1D1C2D9D00E99CD9 /* Sources */,
 				043999F01D1C2D9D00E99CD9 /* Frameworks */,
 				043999F11D1C2D9D00E99CD9 /* Resources */,
+				645BDD6424B5FEFF009129B1 /* Embed Frameworks */,
 			);
 			buildRules = (
 			);
@@ -1852,6 +1929,12 @@
 			mainGroup = 043999EA1D1C2D9D00E99CD9;
 			productRefGroup = 043999F41D1C2D9D00E99CD9 /* Products */;
 			projectDirPath = "";
+			projectReferences = (
+				{
+					ProductGroup = 6452143824B4AB43007203D5 /* Products */;
+					ProjectRef = 6452143724B4AB43007203D5 /* WhirlyGlobeMaplyComponent.xcodeproj */;
+				},
+			);
 			projectRoot = "";
 			targets = (
 				043999F21D1C2D9D00E99CD9 /* Ring */,
@@ -1862,6 +1945,23 @@
 		};
 /* End PBXProject section */
 
+/* Begin PBXReferenceProxy section */
+		6452143D24B4AB44007203D5 /* WhirlyGlobeMaplyComponent.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = WhirlyGlobeMaplyComponent.framework;
+			remoteRef = 6452143C24B4AB44007203D5 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		6452143F24B4AB44007203D5 /* WhirlyGlobeMaplyComponentTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = WhirlyGlobeMaplyComponentTests.xctest;
+			remoteRef = 6452143E24B4AB44007203D5 /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+/* End PBXReferenceProxy section */
+
 /* Begin PBXResourcesBuildPhase section */
 		043999F11D1C2D9D00E99CD9 /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
@@ -1902,7 +2002,9 @@
 				1A5DC03E1F35678D0075E8EF /* ContactRequestsViewController.storyboard in Resources */,
 				446FAF192373424700519C4F /* SendFileViewController.storyboard in Resources */,
 				0EF49AA323828CD00064CD98 /* ConferenceParticipantView.xib in Resources */,
+				645BDD7724B7415A009129B1 /* MessageCellLocationSharingSent.xib in Resources */,
 				0E72374A20460320006B0C7D /* ProfileHeaderView.xib in Resources */,
+				64F8127824B8AA5200A7DE6A /* MessageCellLocationSharingReceived.xib in Resources */,
 				4430A66D236CBC5900747177 /* ContactPickerViewController.storyboard in Resources */,
 				0E96ED75225D06250016C07D /* GeneralSettingsViewController.storyboard in Resources */,
 				0EB1A5CF1F8EBE03009923E2 /* DeviceCell.xib in Resources */,
@@ -2056,6 +2158,7 @@
 				0E438A9A204F47E700402900 /* SettingsTableView.swift in Sources */,
 				0E49096A1FEAB156005CAA50 /* CallsAdapter.mm in Sources */,
 				1A2D18A61F27F7A400B2C785 /* UIViewController+Rx.swift in Sources */,
+				64F8127724B8AA5200A7DE6A /* MessageCellLocationSharingReceived.swift in Sources */,
 				66E6381221764C2C005EA2B0 /* GrowingTextView.swift in Sources */,
 				0E7CF4DB20164B6700CD967D /* ButtonsContainerView.swift in Sources */,
 				0E49097A1FEAC9E1005CAA50 /* CallViewController.swift in Sources */,
@@ -2116,6 +2219,7 @@
 				1A20417C1F1E56FF00C08435 /* WelcomeViewModel.swift in Sources */,
 				1A5DC03D1F35678D0075E8EF /* ContactRequestItem.swift in Sources */,
 				0E403F811F7D797300C80BC2 /* MessageCellGenerated.swift in Sources */,
+				64F8127A24BBC19C00A7DE6A /* MessageCellLocationSharing.swift in Sources */,
 				0E6D959C2407116E00996A28 /* LinkToAccountManagerViewModel.swift in Sources */,
 				62AD584C2056DB2700AF0701 /* MessageCellDataTransferSent.swift in Sources */,
 				0ECA5683243394960055D31E /* MigrateAccountViewController.swift in Sources */,
@@ -2155,6 +2259,7 @@
 				1A5DC0281F3564AA0075E8EF /* MessageModel.swift in Sources */,
 				0E3697A1203235EA009A68CA /* BannedContactItem.swift in Sources */,
 				56BBC9DF1EDDC9D300CDAF8B /* LookupNameResponse.m in Sources */,
+				645BDD7B24B7415A009129B1 /* MessageCellLocationSharingSent.swift in Sources */,
 				66F295DE2166A5930044ED6F /* Devices+Helpers.swift in Sources */,
 				1A2041911F1FD46300C08435 /* DesignableView.swift in Sources */,
 				0ED2B6FE1F96A16C001572F0 /* LinkNewDeviceViewModel.swift in Sources */,
@@ -2199,6 +2304,7 @@
 				02B22DFF1DF755DB000358C9 /* AccountsService.swift in Sources */,
 				1A5DC0421F3567DF0075E8EF /* ContactRequestsCoordinator.swift in Sources */,
 				62E55B6F1F793ADE00D3FEF4 /* AvatarsColors.swift in Sources */,
+				645BDD8124B74BCB009129B1 /* LocationSharingService.swift in Sources */,
 				1A5DC0401F35678D0075E8EF /* ContactRequestsViewModel.swift in Sources */,
 				56BBC9A21ED714DF00CDAF8B /* MessagesAdapterDelegate.swift in Sources */,
 				1A5DC0301F3565AE0075E8EF /* SmartlistViewController.swift in Sources */,
@@ -2437,6 +2543,7 @@
 				HEADER_SEARCH_PATHS = (
 					"$(SRCROOT)/../fat/include",
 					"$(SRCROOT)/../../daemon/contrib/native-arm64/ffmpeg",
+					"$(SRCROOT)/WhirlyGlobeMaply/ios/library/WhirlyGlobe-MaplyComponent/include/**",
 				);
 				INFOPLIST_FILE = Ring/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
@@ -2447,7 +2554,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE = "";
 				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_OBJC_BRIDGING_HEADER = "Ring/Bridging/Ring-Bridging-Header.h";
+				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Ring/Bridging/Ring-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_SWIFT3_OBJC_INFERENCE = Off;
 				SWIFT_VERSION = 5.0;
@@ -2478,6 +2585,7 @@
 				HEADER_SEARCH_PATHS = (
 					"$(SRCROOT)/../fat/include",
 					"$(SRCROOT)/../../daemon/contrib/native-arm64/ffmpeg",
+					"$(SRCROOT)/WhirlyGlobeMaply/ios/library/WhirlyGlobe-MaplyComponent/include/**",
 				);
 				INFOPLIST_FILE = Ring/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
@@ -2488,7 +2596,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE = "";
 				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_OBJC_BRIDGING_HEADER = "Ring/Bridging/Ring-Bridging-Header.h";
+				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Ring/Bridging/Ring-Bridging-Header.h";
 				SWIFT_SWIFT3_OBJC_INFERENCE = Off;
 				SWIFT_VERSION = 5.0;
 				VALID_ARCHS = arm64;
@@ -2645,6 +2753,7 @@
 				HEADER_SEARCH_PATHS = (
 					"$(SRCROOT)/../fat/include",
 					"$(SRCROOT)/../../daemon/contrib/native-arm64/ffmpeg",
+					"$(SRCROOT)/WhirlyGlobeMaply/ios/library/WhirlyGlobe-MaplyComponent/include/**",
 				);
 				INFOPLIST_FILE = Ring/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
@@ -2655,7 +2764,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE = "";
 				PROVISIONING_PROFILE_SPECIFIER = "";
-				SWIFT_OBJC_BRIDGING_HEADER = "Ring/Bridging/Ring-Bridging-Header.h";
+				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/Ring/Bridging/Ring-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_SWIFT3_OBJC_INFERENCE = Off;
 				SWIFT_VERSION = 5.0;
diff --git a/Ring/Ring/AppDelegate.swift b/Ring/Ring/AppDelegate.swift
index 9a529c39b84a449813f2aade24f1e21fdbf9ad4c..8cbe43887230a7fd4efd15d41f4c855e6c647ec5 100644
--- a/Ring/Ring/AppDelegate.swift
+++ b/Ring/Ring/AppDelegate.swift
@@ -1,10 +1,11 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com>
  *  Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com>
  *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
  *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -63,6 +64,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
     private lazy var conversationsService: ConversationsService = {
         ConversationsService(withMessageAdapter: MessagesAdapter(), dbManager: self.dBManager)
     }()
+    private lazy var locationSharingService: LocationSharingService = {
+        LocationSharingService(dbManager: self.dBManager)
+    }()
 
     private let voipRegistry = PKPushRegistry(queue: DispatchQueue.main)
 
@@ -79,7 +83,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                             withAudioService: self.audioService,
                             withDataTransferService: self.dataTransferService,
                             withProfileService: self.profileService,
-                            withCallsProvider: self.callsProvider)
+                            withCallsProvider: self.callsProvider,
+                            withLocationSharingService: self.locationSharingService)
     }()
     private lazy var appCoordinator: AppCoordinator = {
         return AppCoordinator(with: self.injectionBag)
@@ -142,7 +147,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
                                                         accountsService: self.accountService,
                                                         nameService: self.nameService,
                                                         dataTransferService: self.dataTransferService,
-                                                        callService: self.callService)
+                                                        callService: self.callService,
+                                                        locationSharingService: self.locationSharingService)
         self.window?.rootViewController = self.appCoordinator.rootViewController
         self.window?.makeKeyAndVisible()
 
diff --git a/Ring/Ring/Bridging/Ring-Bridging-Header.h b/Ring/Ring/Bridging/Ring-Bridging-Header.h
index b72f74b306fb8cdfd138d86bf1e5354222dfefee..482aec9e86e19714afd49855668990d491403041 100644
--- a/Ring/Ring/Bridging/Ring-Bridging-Header.h
+++ b/Ring/Ring/Bridging/Ring-Bridging-Header.h
@@ -1,8 +1,9 @@
 /*
- *  Copyright (C) 2016-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2016-2020 Savoir-faire Linux Inc.
  *
  *  Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com>
  *  Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.com>
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -43,3 +44,4 @@
 #import <UserNotifications/UserNotifications.h>
 #import <GSKStretchyHeaderView/GSKStretchyHeaderView.h>
 #import "DataTransferAdapter.h"
+#import "../../WhirlyGlobeMaply/ios/library/WhirlyGlobe-MaplyComponent/include/MaplyBridge.h"
diff --git a/Ring/Ring/Constants/Generated/Images.swift b/Ring/Ring/Constants/Generated/Images.swift
index 164c3d302f0180c22db9ef13c0f48b5b3856aa02..f267109e56652f9db1f5dcbf5e7344a5572a02f4 100644
--- a/Ring/Ring/Constants/Generated/Images.swift
+++ b/Ring/Ring/Constants/Generated/Images.swift
@@ -58,6 +58,7 @@ internal enum Asset {
   internal static let leftArrow = ImageAsset(name: "left_arrow")
   internal static let messageBackgroundColor = ColorAsset(name: "message_background_color")
   internal static let moreSettings = ImageAsset(name: "more_settings")
+  internal static let myLocation = ImageAsset(name: "my_location")
   internal static let pauseCall = ImageAsset(name: "pause_call")
   internal static let phoneBook = ImageAsset(name: "phone_book")
   internal static let qrCode = ImageAsset(name: "qr_code")
@@ -134,7 +135,8 @@ internal struct ImageAsset {
     #if os(iOS) || os(tvOS)
     let image = Image(named: name, in: bundle, compatibleWith: nil)
     #elseif os(macOS)
-    let image = bundle.image(forResource: NSImage.Name(name))
+    let name = NSImage.Name(self.name)
+    let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
     #elseif os(watchOS)
     let image = Image(named: name)
     #endif
diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift
index c7c61167de5d7a9f1077809ad7230b85933d9d33..f4001464d62378ea35825ca6869fb1d71801797c 100644
--- a/Ring/Ring/Constants/Generated/Strings.swift
+++ b/Ring/Ring/Constants/Generated/Strings.swift
@@ -8,7 +8,7 @@ import Foundation
 // MARK: - Strings
 
 // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
-// swiftlint:disable nesting type_body_length type_name
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
 internal enum L10n {
 
   internal enum Account {
@@ -178,10 +178,14 @@ internal enum L10n {
     internal static let deleteAction = L10n.tr("Localizable", "actions.deleteAction")
     /// Done
     internal static let doneAction = L10n.tr("Localizable", "actions.doneAction")
+    /// Go to Settings
+    internal static let goToSettings = L10n.tr("Localizable", "actions.goToSettings")
     ///   Audio Call
     internal static let startAudioCall = L10n.tr("Localizable", "actions.startAudioCall")
     ///   Video Call
     internal static let startVideoCall = L10n.tr("Localizable", "actions.startVideoCall")
+    /// Stop sharing
+    internal static let stopLocationSharing = L10n.tr("Localizable", "actions.stopLocationSharing")
   }
 
   internal enum Alerts {
@@ -201,6 +205,8 @@ internal enum L10n {
     internal static let accountNoNetworkMessage = L10n.tr("Localizable", "alerts.accountNoNetworkMessage")
     /// Can't connect to the network
     internal static let accountNoNetworkTitle = L10n.tr("Localizable", "alerts.accountNoNetworkTitle")
+    /// Already sharing location with this user
+    internal static let alreadylocationSharing = L10n.tr("Localizable", "alerts.alreadylocationSharing")
     /// Are you sure you want to block this contact? The conversation history with this contact will also be deleted permanently.
     internal static let confirmBlockContact = L10n.tr("Localizable", "alerts.confirmBlockContact")
     /// Block Contact
@@ -227,10 +233,28 @@ internal enum L10n {
     internal static let incomingCallButtonAccept = L10n.tr("Localizable", "alerts.incomingCallButtonAccept")
     /// Ignore
     internal static let incomingCallButtonIgnore = L10n.tr("Localizable", "alerts.incomingCallButtonIgnore")
+    /// Turn on "Location Services" to allow "Jami" to determine your location.
+    internal static let locationServiceIsDisabled = L10n.tr("Localizable", "alerts.locationServiceIsDisabled")
+    /// Share my location
+    internal static let locationSharing = L10n.tr("Localizable", "alerts.locationSharing")
+    /// 10 min
+    internal static let locationSharingDuration10min = L10n.tr("Localizable", "alerts.locationSharingDuration10min")
+    /// 1 hour
+    internal static let locationSharingDuration1hour = L10n.tr("Localizable", "alerts.locationSharingDuration1hour")
+    /// How long should the location sharing be?
+    internal static let locationSharingDurationTitle = L10n.tr("Localizable", "alerts.locationSharingDurationTitle")
+    /// Map information
+    internal static let mapInformation = L10n.tr("Localizable", "alerts.mapInformation")
     /// Access to photo library not granted
     internal static let noLibraryPermissionsTitle = L10n.tr("Localizable", "alerts.noLibraryPermissionsTitle")
+    /// Access to location not granted
+    internal static let noLocationPermissionsTitle = L10n.tr("Localizable", "alerts.noLocationPermissionsTitle")
     /// Media permission not granted
     internal static let noMediaPermissionsTitle = L10n.tr("Localizable", "alerts.noMediaPermissionsTitle")
+    /// © OpenStreetMap contributors
+    internal static let openStreetMapCopyright = L10n.tr("Localizable", "alerts.openStreetMapCopyright")
+    /// Learn more
+    internal static let openStreetMapCopyrightMoreInfo = L10n.tr("Localizable", "alerts.openStreetMapCopyrightMoreInfo")
     /// Cancel
     internal static let profileCancelPhoto = L10n.tr("Localizable", "alerts.profileCancelPhoto")
     /// Take photo
@@ -398,6 +422,8 @@ internal enum L10n {
     internal static let invitationAccepted = L10n.tr("Localizable", "generatedMessage.invitationAccepted")
     /// Invitation received
     internal static let invitationReceived = L10n.tr("Localizable", "generatedMessage.invitationReceived")
+    /// Live location sharing
+    internal static let liveLocationSharing = L10n.tr("Localizable", "generatedMessage.liveLocationSharing")
     /// Missed incoming call
     internal static let missedIncomingCall = L10n.tr("Localizable", "generatedMessage.missedIncomingCall")
     /// Missed outgoing call
@@ -501,6 +527,10 @@ internal enum L10n {
     internal static let acceptCall = L10n.tr("Localizable", "notifications.acceptCall")
     /// Incoming Call
     internal static let incomingCall = L10n.tr("Localizable", "notifications.incomingCall")
+    /// Incoming location sharing started
+    internal static let locationSharingStarted = L10n.tr("Localizable", "notifications.locationSharingStarted")
+    /// Incoming location sharing stopped
+    internal static let locationSharingStopped = L10n.tr("Localizable", "notifications.locationSharingStopped")
     /// Missed Call
     internal static let missedCall = L10n.tr("Localizable", "notifications.missedCall")
     /// New file
@@ -559,7 +589,7 @@ internal enum L10n {
   }
 }
 // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
-// swiftlint:enable nesting type_body_length type_name
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
 
 // MARK: - Implementation Details
 
@@ -572,8 +602,6 @@ extension L10n {
 
 // swiftlint:disable convenience_type
 private final class BundleToken {
-  static let bundle: Bundle = {
-    Bundle(for: BundleToken.self)
-  }()
+  static let bundle = Bundle(for: BundleToken.self)
 }
 // swiftlint:enable convenience_type
diff --git a/Ring/Ring/Coordinators/InjectionBag.swift b/Ring/Ring/Coordinators/InjectionBag.swift
index b15d5041277545181e28f622d733af9994e46a07..e343ec3b842b7ae1d8427e73bc80ad353eb3f2e1 100644
--- a/Ring/Ring/Coordinators/InjectionBag.swift
+++ b/Ring/Ring/Coordinators/InjectionBag.swift
@@ -1,7 +1,8 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -36,6 +37,7 @@ class InjectionBag {
     let dataTransferService: DataTransferService
     let profileService: ProfilesService
     let callsProvider: CallsProviderDelegate
+    let locationSharingService: LocationSharingService
 
     init (withDaemonService daemonService: DaemonService,
           withAccountService accountService: AccountsService,
@@ -49,7 +51,8 @@ class InjectionBag {
           withAudioService audioService: AudioService,
           withDataTransferService dataTransferService: DataTransferService,
           withProfileService profileService: ProfilesService,
-          withCallsProvider callsProvider: CallsProviderDelegate) {
+          withCallsProvider callsProvider: CallsProviderDelegate,
+          withLocationSharingService locationSharingService: LocationSharingService) {
         self.daemonService = daemonService
         self.accountService = accountService
         self.nameService = nameService
@@ -63,5 +66,6 @@ class InjectionBag {
         self.dataTransferService = dataTransferService
         self.profileService = profileService
         self.callsProvider = callsProvider
+        self.locationSharingService = locationSharingService
     }
 }
diff --git a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift
index 1fad9f333906f4f0bac76f3a5cc947c91ffd2aba..bf0296dce77e5224f6a4887d464b0991c32a214a 100644
--- a/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift
+++ b/Ring/Ring/Database/DBHelpers/InteractionDataHelper.swift
@@ -152,37 +152,10 @@ final class InteractionDataHelper {
         }
     }
 
-    func delete (item: Interaction, dataBase: Connection) -> Bool {
-        let interactionId = item.id
-        let query = table.filter(id == interactionId)
-        do {
-            let deletedRows = try dataBase.run(query.delete())
-            guard deletedRows == 1 else {
-                return false
-            }
-            return true
-        } catch _ {
-            return false
-        }
-    }
-
-    func delete (interactionId: Int64, dataBase: Connection) -> Bool {
-        let query = table.filter(id == interactionId)
-        do {
-            let deletedRows = try dataBase.run(query.delete())
-            guard deletedRows == 1 else {
-                return false
-            }
-            return true
-        } catch _ {
-            return false
-        }
-    }
-
     func selectInteraction (interactionId: Int64, dataBase: Connection) throws -> Interaction? {
         let query = table.filter(id == interactionId)
         let items = try dataBase.prepare(query)
-        for item in  items {
+        for item in items {
             return Interaction(id: item[id],
                                author: item[author],
                                conversation: item[conversation],
@@ -215,11 +188,11 @@ final class InteractionDataHelper {
         return interactions
     }
 
-    func selectInteractionsForConversation (conv: Int64, dataBase: Connection) throws -> [Interaction]? {
-        let query = table.filter(conversation == conv)
+    func selectInteractions(where predicat: Expression<Bool>, dataBase: Connection) throws -> [Interaction] {
+        let query = table.filter(predicat)
         var interactions = [Interaction]()
         let items = try dataBase.prepare(query)
-        for item in  items {
+        for item in items {
             interactions.append(Interaction(id: item[id],
                                             author: item[author],
                                             conversation: item[conversation],
@@ -234,27 +207,14 @@ final class InteractionDataHelper {
         return interactions
     }
 
+    func selectInteractionsForConversation(conv: Int64, dataBase: Connection) throws -> [Interaction]? {
+        return try self.selectInteractions(where: conversation == conv, dataBase: dataBase)
+    }
+
     func selectInteractionWithDaemonId(interactionDaemonID: String, dataBase: Connection) throws -> Interaction? {
-        let query = table.filter(daemonId == interactionDaemonID)
-        var interactions = [Interaction]()
-        let items = try dataBase.prepare(query)
-        for item in  items {
-            interactions.append(Interaction(id: item[id],
-                                            author: item[author],
-                                            conversation: item[conversation],
-                                            timestamp: item[timestamp],
-                                            duration: item[duration],
-                                            body: item[body],
-                                            type: item[type],
-                                            status: item[status],
-                                            daemonID: item[daemonId],
-                                            incoming: item[incoming]))
-        }
-        if interactions.isEmpty {
-            return nil
-        }
+        let interactions = try self.selectInteractions(where: daemonId == interactionDaemonID, dataBase: dataBase)
 
-        if interactions.count > 1 {
+        if interactions.isEmpty || interactions.count > 1 {
             return nil
         }
 
@@ -300,14 +260,23 @@ final class InteractionDataHelper {
         }
     }
 
-    func deleteAllIntercations(conv: Int64, dataBase: Connection) -> Bool {
-        let query = table.filter(conversation == conv)
+    func deleteInteractions(where predicat: Expression<Bool>, dataBase: Connection) throws -> Bool {
+        let query = table.filter(predicat)
+        let deletedRows = try dataBase.run(query.delete())
+        return deletedRows > 0
+    }
+
+    func deleteAllInteractions(conv: Int64, dataBase: Connection) -> Bool {
         do {
-            if try dataBase.run(query.delete()) > 0 {
-                return true
-            } else {
-                return false
-            }
+            return try self.deleteInteractions(where: conversation == conv, dataBase: dataBase)
+        } catch {
+            return false
+        }
+    }
+
+    func delete(interactionId: Int64, dataBase: Connection) -> Bool {
+        do {
+            return try self.deleteInteractions(where: id == interactionId, dataBase: dataBase)
         } catch {
             return false
         }
diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift
index 526a3055c691db614544d27666b2b951f7337655..e05f520c9bc9ee91408d3c568502c1615d0a3750 100644
--- a/Ring/Ring/Database/DBManager.swift
+++ b/Ring/Ring/Database/DBManager.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
  *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
@@ -153,6 +153,7 @@ enum InteractionType: String {
     case contact    = "CONTACT"
     case iTransfer  = "INCOMING_DATA_TRANSFER"
     case oTransfer  = "OUTGOING_DATA_TRANSFER"
+    case location   = "LOCATION"
 }
 
 typealias SavedMessageForConversation = (messageID: Int64, conversationID: Int64)
@@ -312,7 +313,7 @@ class DBManager {
                 observable.on(.error(DBBridgingError.saveMessageFailed))
             }
             return Disposables.create { }
-            }
+        }
     }
 
     func getConversationsObservable(for accountId: String) -> Observable<[ConversationModel]> {
@@ -495,7 +496,7 @@ class DBManager {
                     }
                     if !interactions.isEmpty {
                         if !self.interactionHepler
-                            .deleteAllIntercations(conv: conversationsId, dataBase: dataBase) {
+                            .deleteAllInteractions(conv: conversationsId, dataBase: dataBase) {
                             completable(.error(DBBridgingError.deleteConversationFailed))
                         }
                     }
@@ -660,7 +661,8 @@ class DBManager {
             interaction.type != InteractionType.contact.rawValue &&
             interaction.type != InteractionType.call.rawValue &&
             interaction.type != InteractionType.iTransfer.rawValue &&
-            interaction.type != InteractionType.oTransfer.rawValue {
+            interaction.type != InteractionType.oTransfer.rawValue &&
+            interaction.type != InteractionType.location.rawValue {
             return nil
         }
         let content = (interaction.type == InteractionType.call.rawValue
@@ -689,6 +691,9 @@ class DBManager {
                 message.status = status.toMessageStatus()
             }
         }
+        if interaction.type == InteractionType.location.rawValue {
+            message.isLocationSharing = true
+        }
         message.messageId = interaction.id
         return message
     }
@@ -708,7 +713,7 @@ class DBManager {
         let interaction = Interaction(defaultID, author,
                                       conversationID, Int64(timeInterval), Int64(duration),
                                       message.content, interactionType.rawValue,
-                                     status, message.daemonId,
+                                      status, message.daemonId,
                                       message.incoming)
         return self.interactionHepler.insert(item: interaction, dataBase: dataBase)
     }
@@ -781,4 +786,58 @@ class DBManager {
         return try self.conversationHelper
             .selectConversationsForProfile(profileUri: contactUri, dataBase: dataBase)?.first?.id
     }
+
+    // MARK: Location sharing
+    func isFirstLocationIncomingUpdate(incoming: Bool, peerUri: String, accountId: String) -> Bool? {
+        do {
+            guard let dataBase = self.dbConnections.forAccount(account: accountId) else { return nil }
+
+            let conversationId = try self.getConversationsFor(contactUri: peerUri, createIfNotExists: true, dataBase: dataBase, accountId: accountId)
+            let interactions = try self.interactionHepler.selectInteractionsForConversation(conv: conversationId!, dataBase: dataBase)
+
+            var isFirst = true
+            for (interaction) in interactions! where interaction.type == InteractionType.location.rawValue && interaction.incoming == incoming {
+                isFirst = false
+                break
+            }
+            return isFirst
+        } catch {
+            return nil
+        }
+    }
+
+    func deleteLocationUpdates(incoming: Bool, peerUri: String, accountId: String) -> Completable {
+        return Completable.create(subscribe: { [weak self] completable in
+            do {
+                guard let self = self, let dataBase = self.dbConnections.forAccount(account: accountId) else { throw DataAccessError.datastoreConnectionError }
+                let conversationId = try self.getConversationsFor(contactUri: peerUri, createIfNotExists: true, dataBase: dataBase, accountId: accountId)
+
+                let predicat: Expression<Bool> = (self.interactionHepler.conversation == conversationId! &&
+                                                  self.interactionHepler.type == InteractionType.location.rawValue &&
+                                                  self.interactionHepler.incoming == incoming)
+
+                _ = try self.interactionHepler.deleteInteractions(where: predicat, dataBase: dataBase)
+                completable(.completed)
+            } catch {
+                completable(.error(DBBridgingError.deleteMessageFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func deleteAllLocationUpdates(accountIds: [String]) -> Bool {
+        var didNotFailOnce = true
+        for accountId in accountIds {
+            do {
+                guard let dataBase = self.dbConnections.forAccount(account: accountId) else { throw DataAccessError.datastoreConnectionError }
+
+                let predicat: Expression<Bool> = (self.interactionHepler.type == InteractionType.location.rawValue)
+
+                _ = try self.interactionHepler.deleteInteractions(where: predicat, dataBase: dataBase)
+            } catch {
+                didNotFailOnce = false
+            }
+        }
+        return didNotFailOnce
+    }
 }
diff --git a/Ring/Ring/Extensions/UIView+Ring.swift b/Ring/Ring/Extensions/UIView+Ring.swift
index ba7ba6e7be46fc9a8b78d2c594e798bc6c02e8e6..accbcfe8fc6b1bbc3033e20d95a35cdd9ece490d 100644
--- a/Ring/Ring/Extensions/UIView+Ring.swift
+++ b/Ring/Ring/Extensions/UIView+Ring.swift
@@ -145,12 +145,11 @@ extension UIView {
         return UIColor.clear
     }
 
-    public func convertViewToImage() -> UIImage? {
-        UIGraphicsBeginImageContext(self.bounds.size)
-        self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
-        let image = UIGraphicsGetImageFromCurrentImageContext()
-        UIGraphicsEndImageContext()
-        return image
+    func convertToImage() -> UIImage {
+        let renderer = UIGraphicsImageRenderer(bounds: bounds)
+        return renderer.image { rendererContext in
+            layer.render(in: rendererContext.cgContext)
+        }
     }
 
     func applyGradient(with colours: [UIColor], locations: [NSNumber]? = nil) {
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
index 860cc8250b6a3f2a7c448f05ea8ae1ba1391943a..3fa51ebbe7a8e76db88fbb5271b44c6304ee66b7 100644
--- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@@ -84,7 +84,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
     private var previousBubbleConstraint: CGFloat?
 
     private var longGestureRecognizer: UILongPressGestureRecognizer?
-    private var tapGestureRecognizer: UITapGestureRecognizer?
+    var tapGestureRecognizer: UITapGestureRecognizer?
 
     // MARK: PrepareForReuse
 
@@ -194,7 +194,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
 
     // MARK: Configure
 
-    private func configureTapGesture() {
+    func configureTapGesture() {
         let shownByDefault = !self.timeLabel.isHidden && !showTimeTap.value
         if !shownByDefault {
             self.bubble.isUserInteractionEnabled = true
@@ -220,7 +220,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
         }
     }
 
-    private func onTapGesture() {
+    func onTapGesture() {
         self.prepareForTapGesture()
 
         if self.timeLabel.isHidden {
@@ -233,9 +233,9 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
         self.showTimeTap.accept(true)
     }
 
-    private func configureLongGesture(_ messageId: Int64, _ bubblePosition: BubblePosition, _ isTransfer: Bool) {
+    private func configureLongGesture(_ messageId: Int64, _ bubblePosition: BubblePosition, _ isTransfer: Bool, _ isLocationSharingBubble: Bool) {
         self.messageId = messageId
-        self.isCopyable = bubblePosition != .generated && !isTransfer
+        self.isCopyable = bubblePosition != .generated && !isTransfer && !isLocationSharingBubble
 
         self.bubble.isUserInteractionEnabled = true
         longGestureRecognizer = UILongPressGestureRecognizer()
@@ -412,7 +412,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
         self.configureCellTimeLabel(item)
 
         self.prepareForReuseLongGesture()
-        self.configureLongGesture(item.message.messageId, item.bubblePosition(), item.isTransfer)
+        self.configureLongGesture(item.message.messageId, item.bubblePosition(), item.isTransfer, item.isLocationSharingBubble)
 
         self.prepareForReuseTapGesture()
         self.configureTapGesture()
@@ -432,6 +432,13 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
             return
 
         case .sent:
+            guard !item.isLocationSharingBubble else {
+                self.setCellTimeLabelVisibility(hide: false)
+                self.bubbleTopConstraint.constant = 32
+                self.bubbleBottomConstraint.constant = 1
+                break
+            }
+
             self.configureTransferCell(item, conversationViewModel)
 
             self.applyBubbleStyleToCell(items, cellForRowAt: indexPath)
@@ -455,6 +462,14 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate {
             }
 
         case .received:
+            guard !item.isLocationSharingBubble else {
+                self.setCellTimeLabelVisibility(hide: false)
+                self.bubbleTopConstraint.constant = 32
+                self.bubbleBottomConstraint.constant = 1
+                self.configureReceivedMessageAvatar(item.sequencing, conversationViewModel)
+                break
+            }
+
             self.configureTransferCell(item, conversationViewModel)
 
             self.applyBubbleStyleToCell(items, cellForRowAt: indexPath)
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharing.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharing.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a361cfa96649c6f8345a9440541a70a58e88fd78
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharing.swift
@@ -0,0 +1,398 @@
+/*
+ *  Copyright (C) 2020 Savoir-faire Linux Inc.
+ *
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import UIKit
+import Reusable
+import RxCocoa
+
+class MessageCellLocationSharing: MessageCell {
+
+    private static let osmCopyrightAndLicenseURL = "https://www.openstreetmap.org/copyright"
+    private static let remoteTileSourceBaseUrl = MessageCellLocationSharing.getBaseURL()
+
+    @IBOutlet weak var bubbleHeight: NSLayoutConstraint!
+
+    var xButton: UIButton?
+    var myPositionButton: UIButton?
+
+    let locationTapped = BehaviorRelay<(Bool, Bool)>(value: (false, false)) // (shouldAnimate, expanding)
+
+    var maplyViewController: MaplyBaseViewController? // protected in Swift?
+    /// The usage of this variable allows for the view to not be refreshed on reuse (e.g. when scrolling)
+    private var preventUnnecessaryReuseCounter = 0
+
+    override func willRemoveSubview(_ subview: UIView) {
+        super.willRemoveSubview(subview)
+        self.preventUnnecessaryReuseCounter = 0
+    }
+
+    override func configureFromItem(_ conversationViewModel: ConversationViewModel, _ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) {
+        super.configureFromItem(conversationViewModel, items, cellForRowAt: indexPath)
+
+        self.shrink()
+
+        if self.maplyViewController as? MaplyViewController == nil || preventUnnecessaryReuseCounter < 2 {
+            self.setupMaply()
+            self.displayMapTile()
+
+            self.configureTapGesture()
+            self.setupOSMCopyrightButton()
+            preventUnnecessaryReuseCounter += 1
+        }
+    }
+
+    override func configureTapGesture() {
+        self.bubble.isUserInteractionEnabled = true
+        self.tapGestureRecognizer = UITapGestureRecognizer()
+        self.tapGestureRecognizer!.rx.event.bind(onNext: { [weak self] _ in self?.onTapGesture() }).disposed(by: self.disposeBag)
+        self.bubble.addGestureRecognizer(tapGestureRecognizer!)
+    }
+
+    override func onTapGesture() {
+        if !locationTapped.value.1 {
+            self.expandOrShrink()
+        }
+    }
+
+    private func removeTapDefaultGestureFromMaply() {
+        if let whirlyKitEAGLView = (self.maplyViewController as? MaplyViewController)?.view.subviews[0],
+           let gesture = whirlyKitEAGLView.gestureRecognizers?.first(where: { (gesture) -> Bool in gesture is UITapGestureRecognizer }) {
+            whirlyKitEAGLView.removeGestureRecognizer(gesture)
+        }
+    }
+
+    private func setupMaply() {
+        self.maplyViewController?.view.removeFromSuperview()
+
+        self.maplyViewController = MaplyViewController(mapType: .typeFlat)
+        self.removeTapDefaultGestureFromMaply()
+
+        self.bubble.addSubview(self.maplyViewController!.view)
+        self.maplyViewController!.view.frame = self.bubble.bounds
+    }
+
+    private func displayMapTile() {
+        self.maplyViewController!.clearColor = UIColor.white
+
+        // thirty fps if we can get it
+        self.maplyViewController!.frameInterval = 2
+
+        if let layer = MessageCellLocationSharing.getMaplyLayer() {
+            layer.handleEdges = false
+            layer.coverPoles = false
+            layer.requireElev = false
+            layer.waitLoad = false
+            layer.drawPriority = 0
+            layer.singleLevelLoading = false
+            self.maplyViewController!.add(layer)
+        } else {
+            self.log.error("[MessageCellLocationSharing] Could not get the layer")
+        }
+
+        if let mapViewC = self.maplyViewController as? MaplyViewController {
+            self.toggleMaplyGesture(false)
+            mapViewC.height = 0.0001
+        }
+    }
+
+    private func toggleMaplyGesture(_ value: Bool) {
+        if let mapViewC = self.maplyViewController as? MaplyViewController {
+            mapViewC.panGesture = value
+            mapViewC.pinchGesture = value
+            mapViewC.rotateGesture = value
+            mapViewC.twoFingerTapGesture = value
+            mapViewC.doubleTapDragGesture = value
+            mapViewC.doubleTapZoomGesture = value
+        }
+    }
+
+    private static func getMaplyLayer() -> MaplyQuadImageTilesLayer? {
+        let layer: MaplyQuadImageTilesLayer
+
+        // Because this is a remote tile set, we'll want a cache directory
+        let baseCacheDir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
+        let tilesCacheDir = "\(baseCacheDir)/openstreetmap/"
+        let maxZoom = Int32(19)
+
+        guard let tileSource =
+            MaplyRemoteTileSource(baseURL: MessageCellLocationSharing.remoteTileSourceBaseUrl,
+                                  ext: "png",
+                                  minZoom: 0,
+                                  maxZoom: maxZoom) else { return nil }
+        tileSource.cacheDir = tilesCacheDir
+        layer = MaplyQuadImageTilesLayer(tileSource: tileSource)!
+
+        return layer
+    }
+
+    private static func getBaseURL() -> String {
+        // OpenStreetMap Tiles, © OpenStreetMap contributors
+        let urls = ["https://a.tile.openstreetmap.org/",
+                    "https://b.tile.openstreetmap.org/",
+                    "https://c.tile.openstreetmap.org/"]
+        let rngIndex = Int.random(in: 0 ..< 3)
+
+        return urls[rngIndex]
+    }
+}
+
+// For children
+extension MessageCellLocationSharing {
+    func updateLocationAndMarker(location: CLLocationCoordinate2D,
+                                 imageData: Data?,
+                                 username: String?,
+                                 marker: MaplyScreenMarker,
+                                 markerDump: MaplyComponentObject?) -> MaplyComponentObject? {
+        // only the first time
+        if markerDump != nil {
+            if let imageData = imageData, let circledImage = UIImage(data: imageData)?.circleMasked {
+                marker.image = circledImage
+            } else {
+                marker.image = AvatarView(profileImageData: nil, username: username ?? "", size: 24).convertToImage()
+            }
+             marker.size = CGSize(width: 24, height: 24)
+        }
+
+        let maplyCoordonate = MaplyCoordinateMakeWithDegrees(Float(location.longitude), Float(location.latitude))
+
+        marker.loc.x = maplyCoordonate.x
+        marker.loc.y = maplyCoordonate.y
+
+        var dumpToReturn: MaplyComponentObject?
+
+        if let mapViewC = self.maplyViewController as? MaplyViewController {
+            if markerDump != nil {
+                self.maplyViewController!.remove(markerDump!)
+            }
+            dumpToReturn = self.maplyViewController!.addScreenMarkers([marker], desc: nil)
+
+            if !locationTapped.value.1 {
+                mapViewC.animate(toPosition: maplyCoordonate, time: 0.1)
+            }
+        }
+        return dumpToReturn
+    }
+}
+
+// For OSM Copyrights
+extension MessageCellLocationSharing {
+    private func setupOSMCopyrightButton() {
+        let infoButton = UIButton(type: .detailDisclosure)
+        infoButton.backgroundColor = UIColor.init(white: 0.75, alpha: 0.25)
+        infoButton.cornerRadius = infoButton.frame.height / 2.0
+        self.bubble.addSubview(infoButton)
+
+        infoButton.translatesAutoresizingMaskIntoConstraints = false
+        let constraintX = NSLayoutConstraint(item: infoButton,
+                                             attribute: NSLayoutConstraint.Attribute.centerX,
+                                             relatedBy: NSLayoutConstraint.Relation.equal,
+                                             toItem: self.bubble,
+                                             attribute: NSLayoutConstraint.Attribute.right,
+                                             multiplier: 1,
+                                             constant: -28)
+        let constraintY = NSLayoutConstraint(item: infoButton,
+                                             attribute: NSLayoutConstraint.Attribute.centerY,
+                                             relatedBy: NSLayoutConstraint.Relation.equal,
+                                             toItem: self.bubble,
+                                             attribute: NSLayoutConstraint.Attribute.bottom,
+                                             multiplier: 1,
+                                             constant: -28)
+        NSLayoutConstraint.activate([constraintX, constraintY])
+        infoButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
+    }
+
+    @objc func buttonAction(sender: UIButton!) {
+        let alert = UIAlertController.init(title: L10n.Alerts.mapInformation,
+                                           message: L10n.Alerts.openStreetMapCopyright,
+                                           preferredStyle: .alert)
+        alert.addAction(.init(title: L10n.Alerts.openStreetMapCopyrightMoreInfo, style: UIAlertAction.Style.default, handler: { (_) in
+            if let url = URL(string: MessageCellLocationSharing.osmCopyrightAndLicenseURL), UIApplication.shared.canOpenURL(url) {
+                UIApplication.shared.open(url, completionHandler: nil)
+            }
+        }))
+        alert.addAction(.init(title: L10n.Global.ok, style: UIAlertAction.Style.cancel))
+
+        self.window?.rootViewController?.present(alert, animated: true, completion: nil)
+    }
+}
+
+// For bigger map
+extension MessageCellLocationSharing {
+    private func shrink() {
+        if self.bubbleHeight.constant > 220 {
+            self.expandOrShrink()
+        }
+    }
+
+    private func expandOrShrink() {
+        let shouldExpand = !self.locationTapped.value.1
+
+        self.updateHeight(shouldExpand)
+        //self.updateWidth(shouldExpand) now in controller, for animation
+        self.toggleMaplyGesture(shouldExpand)
+
+        if shouldExpand {
+            self.setupXButton()
+            self.setupMyPositionButton()
+        } else {
+            self.removeXButton()
+            self.removeMyPositionButton()
+        }
+
+        self.locationTapped.accept((true, shouldExpand))
+    }
+
+    private func updateHeight(_ shouldExpand: Bool, extendedHeight: CGFloat = 350) {
+        let normalHeight: CGFloat = 220
+        if shouldExpand {
+            self.bubbleHeight.constant = extendedHeight
+        } else {
+            self.bubbleHeight.constant = normalHeight
+        }
+    }
+
+    @objc func updateWidth(_ shouldExpand: Bool) {
+        fatalError("Must override this function")
+    }
+
+    func expandHeight(_ shouldExpand: Bool, _ height: CGFloat) {
+        if shouldExpand {
+            let percentage: CGFloat = self.hasTopNotch() ? 0.88 : 0.91
+
+            self.updateHeight(shouldExpand,
+                              extendedHeight: (height * percentage) - self.bubbleTopConstraint.constant)
+        }
+    }
+
+    func hasTopNotch() -> Bool {
+        if #available(iOS 13.0, *) {
+            return UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.safeAreaInsets.top ?? 0 > 20
+        } else {
+            return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20
+        }
+    }
+}
+
+extension MessageCellLocationSharing {
+    private func setupXButton() {
+        self.xButton = UIButton()
+        let xButton = self.xButton!
+
+        xButton.setBackgroundImage(UIImage(asset: Asset.closeIcon)!, for: UIControl.State.normal)
+        xButton.backgroundColor = UIColor.init(white: 0.25, alpha: 0.50)
+        xButton.cornerRadius = 5
+        self.bubble.addSubview(xButton)
+
+        xButton.translatesAutoresizingMaskIntoConstraints = false
+        let constraintX = NSLayoutConstraint(item: xButton,
+                                             attribute: NSLayoutConstraint.Attribute.centerX,
+                                             relatedBy: NSLayoutConstraint.Relation.equal,
+                                             toItem: self.bubble,
+                                             attribute: NSLayoutConstraint.Attribute.left,
+                                             multiplier: 1,
+                                             constant: 28)
+        let constraintY = NSLayoutConstraint(item: xButton,
+                                             attribute: NSLayoutConstraint.Attribute.centerY,
+                                             relatedBy: NSLayoutConstraint.Relation.equal,
+                                             toItem: self.bubble,
+                                             attribute: NSLayoutConstraint.Attribute.top,
+                                             multiplier: 1,
+                                             constant: 28)
+        let height = NSLayoutConstraint(item: xButton,
+                                        attribute: NSLayoutConstraint.Attribute.height,
+                                        relatedBy: NSLayoutConstraint.Relation.equal,
+                                        toItem: nil,
+                                        attribute: NSLayoutConstraint.Attribute.notAnAttribute,
+                                        multiplier: 1,
+                                        constant: 32)
+        let width = NSLayoutConstraint(item: xButton,
+                                       attribute: NSLayoutConstraint.Attribute.width,
+                                       relatedBy: NSLayoutConstraint.Relation.equal,
+                                       toItem: nil,
+                                       attribute: NSLayoutConstraint.Attribute.notAnAttribute,
+                                       multiplier: 1,
+                                       constant: 32)
+        NSLayoutConstraint.activate([constraintX, constraintY, height, width])
+        xButton.addTarget(self, action: #selector(XButtonAction), for: .touchUpInside)
+    }
+
+    private func removeXButton() {
+        self.xButton?.removeFromSuperview()
+        self.xButton = nil
+    }
+
+    @objc func XButtonAction(sender: UIButton!) {
+        self.expandOrShrink()
+    }
+}
+
+extension MessageCellLocationSharing {
+    private func setupMyPositionButton() {
+        if self as? MessageCellLocationSharingSent != nil {
+            self.myPositionButton = UIButton()
+            let myLocation = self.myPositionButton!
+            myLocation.setImage(UIImage(asset: Asset.myLocation)!, for: .normal)
+            myLocation.backgroundColor = UIColor.init(white: 0.25, alpha: 0.50)
+            myLocation.cornerRadius = 5
+            self.bubble.addSubview(myLocation)
+
+            myLocation.translatesAutoresizingMaskIntoConstraints = false
+            let constraintX = NSLayoutConstraint(item: myLocation,
+                                                 attribute: NSLayoutConstraint.Attribute.centerX,
+                                                 relatedBy: NSLayoutConstraint.Relation.equal,
+                                                 toItem: self.bubble,
+                                                 attribute: NSLayoutConstraint.Attribute.right,
+                                                 multiplier: 1,
+                                                 constant: -28)
+            let constraintY = NSLayoutConstraint(item: myLocation,
+                                                 attribute: NSLayoutConstraint.Attribute.centerY,
+                                                 relatedBy: NSLayoutConstraint.Relation.equal,
+                                                 toItem: self.bubble,
+                                                 attribute: NSLayoutConstraint.Attribute.bottom,
+                                                 multiplier: 1,
+                                                 constant: -70)
+            let height = NSLayoutConstraint(item: myLocation,
+                                            attribute: NSLayoutConstraint.Attribute.height,
+                                            relatedBy: NSLayoutConstraint.Relation.equal,
+                                            toItem: nil,
+                                            attribute: NSLayoutConstraint.Attribute.notAnAttribute,
+                                            multiplier: 1,
+                                            constant: 32)
+            let width = NSLayoutConstraint(item: myLocation,
+                                           attribute: NSLayoutConstraint.Attribute.width,
+                                           relatedBy: NSLayoutConstraint.Relation.equal,
+                                           toItem: nil,
+                                           attribute: NSLayoutConstraint.Attribute.notAnAttribute,
+                                           multiplier: 1,
+                                           constant: 32)
+            NSLayoutConstraint.activate([constraintX, constraintY, height, width])
+            myLocation.addTarget(self, action: #selector(myPositionButtonAction), for: .touchUpInside)
+        }
+    }
+
+    private func removeMyPositionButton() {
+        self.myPositionButton?.removeFromSuperview()
+        self.myPositionButton = nil
+    }
+
+    @objc func myPositionButtonAction(sender: UIButton!) {
+        fatalError("Must override this function")
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b6abef26cbdca5d99d8532ba8991df6acbf6b53f
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.swift
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (C) 2020 Savoir-faire Linux Inc.
+ *
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import UIKit
+import Reusable
+
+class MessageCellLocationSharingReceived: MessageCellLocationSharing {
+
+    private var myContactsLocationMarker = MaplyScreenMarker()
+    private var markerComponentObject: MaplyComponentObject?
+
+    @IBOutlet weak var receivedBubbleLeading: NSLayoutConstraint!
+    @IBOutlet weak var receivedBubbleTrailling: NSLayoutConstraint!
+
+    override func configureFromItem(_ conversationViewModel: ConversationViewModel, _ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) {
+        super.configureFromItem(conversationViewModel, items, cellForRowAt: indexPath)
+
+        conversationViewModel.myContactsLocation
+            .subscribe(onNext: { [weak self, weak conversationViewModel] location in
+                guard let self = self, let location = location else { return }
+
+                self.markerComponentObject = self.updateLocationAndMarker(location: location,
+                                                                                    imageData: conversationViewModel?.profileImageData.value,
+                                                                                    username: conversationViewModel?.userName.value,
+                                                                                    marker: self.myContactsLocationMarker,
+                                                                                    markerDump: self.markerComponentObject)
+            })
+            .disposed(by: self.disposeBag)
+    }
+
+    override func updateWidth(_ shouldExpand: Bool) {
+        let normalValue: CGFloat = 116
+        let extendedValue: CGFloat = 16
+        if shouldExpand {
+            self.receivedBubbleTrailling.constant = extendedValue
+            self.receivedBubbleLeading.constant = extendedValue
+        } else {
+            self.receivedBubbleTrailling.constant = normalValue
+            self.receivedBubbleLeading.constant = 64
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.xib
new file mode 100644
index 0000000000000000000000000000000000000000..5e8a8acb993349fd86c47dedc89b439ab2c90f6c
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingReceived.xib
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" rowHeight="253" id="3QB-g7-MaS" userLabel="Message Cell Location Sharing Received" customClass="MessageCellLocationSharingReceived" customModule="Ring" customModuleProvider="target">
+            <rect key="frame" x="0.0" y="0.0" width="510" height="240"/>
+            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" tableViewCell="3QB-g7-MaS" id="Dkz-SA-3Af">
+                <rect key="frame" x="0.0" y="0.0" width="510" height="240"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xVQ-Jk-Sxy" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
+                        <rect key="frame" x="64" y="8" width="330" height="220"/>
+                        <color key="backgroundColor" systemColor="systemGray4Color" red="0.81960784310000001" green="0.81960784310000001" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="220" id="hS0-Te-OEU"/>
+                        </constraints>
+                        <userDefinedRuntimeAttributes>
+                            <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                <integer key="value" value="15"/>
+                            </userDefinedRuntimeAttribute>
+                        </userDefinedRuntimeAttributes>
+                    </view>
+                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="11/14/2016 12:34PM" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4OM-U1-teG" userLabel="Message Time">
+                        <rect key="frame" x="178" y="9" width="154" height="20.5"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                        <nil key="textColor"/>
+                        <nil key="highlightedColor"/>
+                    </label>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Wm5-ce-Sf6" userLabel="Left Divider">
+                        <rect key="frame" x="31" y="19" width="131" height="1"/>
+                        <color key="backgroundColor" red="0.94117647059999998" green="0.94117647059999998" blue="0.94117647059999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="1" id="dEi-Ni-etd"/>
+                        </constraints>
+                    </view>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WgT-u7-Mgl" userLabel="Right Divider">
+                        <rect key="frame" x="348" y="19" width="131" height="1"/>
+                        <color key="backgroundColor" red="0.94117647059999998" green="0.94117647059999998" blue="0.94117647059999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="1" id="9kZ-1u-mwB"/>
+                        </constraints>
+                    </view>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aRL-o9-tZc" userLabel="Avatar View">
+                        <rect key="frame" x="16" y="194" width="32" height="32"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="32" id="JKg-Rh-evV"/>
+                            <constraint firstAttribute="width" constant="32" id="NQC-fC-Mw5"/>
+                        </constraints>
+                    </view>
+                </subviews>
+                <color key="tintColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+                <constraints>
+                    <constraint firstAttribute="trailingMargin" secondItem="WgT-u7-Mgl" secondAttribute="trailing" constant="16" id="6Dc-L8-sJC"/>
+                    <constraint firstItem="WgT-u7-Mgl" firstAttribute="centerY" secondItem="4OM-U1-teG" secondAttribute="centerY" id="ALc-pa-7ZW"/>
+                    <constraint firstItem="4OM-U1-teG" firstAttribute="top" secondItem="Dkz-SA-3Af" secondAttribute="topMargin" constant="-2" id="CbK-m1-TUR"/>
+                    <constraint firstItem="aRL-o9-tZc" firstAttribute="trailing" secondItem="xVQ-Jk-Sxy" secondAttribute="leading" constant="-16" id="DIQ-qi-aUb"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="leading" secondItem="Dkz-SA-3Af" secondAttribute="leadingMargin" constant="16" id="Faa-N7-gPP"/>
+                    <constraint firstItem="WgT-u7-Mgl" firstAttribute="leading" secondItem="4OM-U1-teG" secondAttribute="trailing" constant="16" id="Foe-Zm-1oU"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="centerY" secondItem="4OM-U1-teG" secondAttribute="centerY" id="Q4u-AX-3D6"/>
+                    <constraint firstAttribute="bottom" secondItem="xVQ-Jk-Sxy" secondAttribute="bottom" constant="8" id="Qbn-zO-KWj"/>
+                    <constraint firstItem="xVQ-Jk-Sxy" firstAttribute="top" secondItem="Dkz-SA-3Af" secondAttribute="top" constant="8" id="R6Q-PY-A3m"/>
+                    <constraint firstItem="aRL-o9-tZc" firstAttribute="bottom" secondItem="xVQ-Jk-Sxy" secondAttribute="bottom" constant="-2" id="aM1-qu-Iys"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="trailing" secondItem="4OM-U1-teG" secondAttribute="leading" constant="-16" id="dlX-Gh-ImE"/>
+                    <constraint firstItem="xVQ-Jk-Sxy" firstAttribute="leading" secondItem="Dkz-SA-3Af" secondAttribute="leading" constant="64" id="qxE-d6-psE"/>
+                    <constraint firstAttribute="trailing" secondItem="xVQ-Jk-Sxy" secondAttribute="trailing" constant="116" id="uTe-7R-qUz"/>
+                    <constraint firstItem="4OM-U1-teG" firstAttribute="centerX" secondItem="Dkz-SA-3Af" secondAttribute="centerX" id="zxy-XJ-QAl"/>
+                </constraints>
+            </tableViewCellContentView>
+            <connections>
+                <outlet property="avatarView" destination="aRL-o9-tZc" id="ROC-7p-3of"/>
+                <outlet property="bubble" destination="xVQ-Jk-Sxy" id="dRd-NH-FPh"/>
+                <outlet property="bubbleBottomConstraint" destination="Qbn-zO-KWj" id="hKY-TA-wId"/>
+                <outlet property="bubbleHeight" destination="hS0-Te-OEU" id="G9h-6Z-A7m"/>
+                <outlet property="bubbleTopConstraint" destination="R6Q-PY-A3m" id="IQA-QC-eV0"/>
+                <outlet property="leftDivider" destination="Wm5-ce-Sf6" id="EaQ-1G-8Db"/>
+                <outlet property="messageLabelMarginConstraint" destination="CbK-m1-TUR" id="8qD-tP-8QW"/>
+                <outlet property="receivedBubbleLeading" destination="qxE-d6-psE" id="aaK-f3-9Nx"/>
+                <outlet property="receivedBubbleTrailling" destination="uTe-7R-qUz" id="RXi-mN-T16"/>
+                <outlet property="rightDivider" destination="WgT-u7-Mgl" id="k10-3V-ZLw"/>
+                <outlet property="timeLabel" destination="4OM-U1-teG" id="ub4-Z8-CsM"/>
+            </connections>
+            <point key="canvasLocation" x="-411.19999999999999" y="-45.877061469265371"/>
+        </tableViewCell>
+    </objects>
+</document>
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.swift
new file mode 100644
index 0000000000000000000000000000000000000000..78a3777ce27d04cb85d657a0f747f517478d71b2
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.swift
@@ -0,0 +1,77 @@
+/*
+ *  Copyright (C) 2020 Savoir-faire Linux Inc.
+ *
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+import UIKit
+import Reusable
+import RxCocoa
+
+class MessageCellLocationSharingSent: MessageCellLocationSharing {
+
+    private var myLocationMarker = MaplyScreenMarker()
+    private var markerComponentObject: MaplyComponentObject?
+
+    @IBOutlet weak var sentBubbleLeading: NSLayoutConstraint!
+
+    @IBOutlet weak var stopSharingButton: UIButton!
+    @IBAction func stopSharingButton(_ sender: Any) {
+        self.delete(sender)
+    }
+
+    override func configureFromItem(_ conversationViewModel: ConversationViewModel, _ items: [MessageViewModel]?, cellForRowAt indexPath: IndexPath) {
+        super.configureFromItem(conversationViewModel, items, cellForRowAt: indexPath)
+
+        conversationViewModel.myLocation
+            .subscribe(onNext: { [weak self, weak conversationViewModel] location in
+                guard let self = self, let location = location?.coordinate else { return }
+
+                self.markerComponentObject = self.updateLocationAndMarker(location: location,
+                                                                          imageData: conversationViewModel?.myOwnProfileImageData,
+                                                                          username: conversationViewModel?.userName.value,
+                                                                          marker: self.myLocationMarker,
+                                                                          markerDump: self.markerComponentObject)
+            })
+            .disposed(by: self.disposeBag)
+
+        self.setupStopSharingButton()
+    }
+
+    private func setupStopSharingButton() {
+        self.stopSharingButton.setTitle(L10n.Actions.stopLocationSharing, for: .normal)
+        self.stopSharingButton.backgroundColor = UIColor.red
+        self.stopSharingButton.setTitleColor(UIColor.white, for: .normal)
+        self.bubble.addSubview(stopSharingButton)
+    }
+
+    override func myPositionButtonAction(sender: UIButton!) {
+        if let mapViewC = self.maplyViewController as? MaplyViewController {
+            mapViewC.animate(toPosition: self.myLocationMarker.loc, time: 0.5)
+        }
+    }
+
+    override func updateWidth(_ shouldExpand: Bool) {
+        let normalValue: CGFloat = 164
+        let extendedValue: CGFloat = 16
+        if shouldExpand {
+            self.sentBubbleLeading.constant = extendedValue
+        } else {
+            self.sentBubbleLeading.constant = normalValue
+        }
+    }
+}
diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.xib b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.xib
new file mode 100644
index 0000000000000000000000000000000000000000..d9d4da3ff0a5bc9795f039a78ef05565971097f6
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCellLocationSharingSent.xib
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" rowHeight="253" id="3QB-g7-MaS" userLabel="Message Cell Location Sharing Sent" customClass="MessageCellLocationSharingSent" customModule="Ring" customModuleProvider="target">
+            <rect key="frame" x="0.0" y="0.0" width="510" height="240"/>
+            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" tableViewCell="3QB-g7-MaS" id="Dkz-SA-3Af">
+                <rect key="frame" x="0.0" y="0.0" width="510" height="240"/>
+                <autoresizingMask key="autoresizingMask"/>
+                <subviews>
+                    <view clipsSubviews="YES" contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xVQ-Jk-Sxy" customClass="MessageBubble" customModule="Ring" customModuleProvider="target">
+                        <rect key="frame" x="164" y="8" width="330" height="220"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PYf-Lj-ehe">
+                                <rect key="frame" x="134" y="175" width="62" height="34"/>
+                                <inset key="contentEdgeInsets" minX="8" minY="8" maxX="8" maxY="8"/>
+                                <state key="normal" title="Button">
+                                    <color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                </state>
+                                <userDefinedRuntimeAttributes>
+                                    <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                        <real key="value" value="15"/>
+                                    </userDefinedRuntimeAttribute>
+                                </userDefinedRuntimeAttributes>
+                                <connections>
+                                    <action selector="stopSharingButton:" destination="3QB-g7-MaS" eventType="touchUpInside" id="9px-PV-vvz"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <color key="backgroundColor" systemColor="systemGray4Color" red="0.81960784310000001" green="0.81960784310000001" blue="0.83921568629999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstItem="PYf-Lj-ehe" firstAttribute="centerY" secondItem="xVQ-Jk-Sxy" secondAttribute="bottom" constant="-28" id="SPy-c9-P2P"/>
+                            <constraint firstItem="PYf-Lj-ehe" firstAttribute="centerX" secondItem="xVQ-Jk-Sxy" secondAttribute="centerX" id="hQD-IF-ddy"/>
+                            <constraint firstAttribute="height" constant="220" id="hS0-Te-OEU"/>
+                        </constraints>
+                        <userDefinedRuntimeAttributes>
+                            <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
+                                <integer key="value" value="15"/>
+                            </userDefinedRuntimeAttribute>
+                        </userDefinedRuntimeAttributes>
+                    </view>
+                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="11/14/2016 12:34PM" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4OM-U1-teG" userLabel="Message Time">
+                        <rect key="frame" x="178" y="9" width="154" height="21"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                        <nil key="textColor"/>
+                        <nil key="highlightedColor"/>
+                    </label>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Wm5-ce-Sf6" userLabel="Left Divider">
+                        <rect key="frame" x="31" y="19" width="131" height="1"/>
+                        <color key="backgroundColor" red="0.94117647059999998" green="0.94117647059999998" blue="0.94117647059999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="1" id="dEi-Ni-etd"/>
+                        </constraints>
+                    </view>
+                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WgT-u7-Mgl" userLabel="Right Divider">
+                        <rect key="frame" x="348" y="19" width="131" height="1"/>
+                        <color key="backgroundColor" red="0.94117647059999998" green="0.94117647059999998" blue="0.94117647059999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="1" id="9kZ-1u-mwB"/>
+                        </constraints>
+                    </view>
+                </subviews>
+                <color key="tintColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+                <constraints>
+                    <constraint firstAttribute="trailingMargin" secondItem="WgT-u7-Mgl" secondAttribute="trailing" constant="16" id="6Dc-L8-sJC"/>
+                    <constraint firstItem="WgT-u7-Mgl" firstAttribute="centerY" secondItem="4OM-U1-teG" secondAttribute="centerY" id="ALc-pa-7ZW"/>
+                    <constraint firstItem="4OM-U1-teG" firstAttribute="top" secondItem="Dkz-SA-3Af" secondAttribute="topMargin" constant="-2" id="CbK-m1-TUR"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="leading" secondItem="Dkz-SA-3Af" secondAttribute="leadingMargin" constant="16" id="Faa-N7-gPP"/>
+                    <constraint firstItem="WgT-u7-Mgl" firstAttribute="leading" secondItem="4OM-U1-teG" secondAttribute="trailing" constant="16" id="Foe-Zm-1oU"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="centerY" secondItem="4OM-U1-teG" secondAttribute="centerY" id="Q4u-AX-3D6"/>
+                    <constraint firstAttribute="bottom" secondItem="xVQ-Jk-Sxy" secondAttribute="bottom" constant="8" id="Qbn-zO-KWj"/>
+                    <constraint firstItem="xVQ-Jk-Sxy" firstAttribute="top" secondItem="Dkz-SA-3Af" secondAttribute="top" constant="8" id="R6Q-PY-A3m"/>
+                    <constraint firstItem="Wm5-ce-Sf6" firstAttribute="trailing" secondItem="4OM-U1-teG" secondAttribute="leading" constant="-16" id="dlX-Gh-ImE"/>
+                    <constraint firstItem="xVQ-Jk-Sxy" firstAttribute="leading" secondItem="Dkz-SA-3Af" secondAttribute="leading" constant="164" id="qxE-d6-psE"/>
+                    <constraint firstAttribute="trailing" secondItem="xVQ-Jk-Sxy" secondAttribute="trailing" constant="16" id="uTe-7R-qUz"/>
+                    <constraint firstItem="4OM-U1-teG" firstAttribute="centerX" secondItem="Dkz-SA-3Af" secondAttribute="centerX" id="zxy-XJ-QAl"/>
+                </constraints>
+            </tableViewCellContentView>
+            <connections>
+                <outlet property="bubble" destination="xVQ-Jk-Sxy" id="dRd-NH-FPh"/>
+                <outlet property="bubbleBottomConstraint" destination="Qbn-zO-KWj" id="hKY-TA-wId"/>
+                <outlet property="bubbleHeight" destination="hS0-Te-OEU" id="oJg-9j-oa8"/>
+                <outlet property="bubbleTopConstraint" destination="R6Q-PY-A3m" id="IQA-QC-eV0"/>
+                <outlet property="leftDivider" destination="Wm5-ce-Sf6" id="EaQ-1G-8Db"/>
+                <outlet property="messageLabelMarginConstraint" destination="CbK-m1-TUR" id="8qD-tP-8QW"/>
+                <outlet property="rightDivider" destination="WgT-u7-Mgl" id="k10-3V-ZLw"/>
+                <outlet property="sentBubbleLeading" destination="qxE-d6-psE" id="wIa-XC-C9H"/>
+                <outlet property="stopSharingButton" destination="PYf-Lj-ehe" id="qXH-bz-cAH"/>
+                <outlet property="timeLabel" destination="4OM-U1-teG" id="ub4-Z8-CsM"/>
+            </connections>
+            <point key="canvasLocation" x="-411.19999999999999" y="-45.877061469265371"/>
+        </tableViewCell>
+    </objects>
+</document>
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index fbd504194b9aff06a79baf4441eecfd791960df1..15fe75bc11a20870e1a128b2c874129ec7ad3051 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -58,6 +58,8 @@ class ConversationViewController: UIViewController,
 
     var keyboardDismissTapRecognizer: UITapGestureRecognizer!
 
+    private lazy var locationManager: CLLocationManager = { return CLLocationManager() }()
+
     func setIsComposing(isComposing: Bool) {
         self.viewModel.setIsComposingMsg(isComposing: isComposing)
     }
@@ -166,7 +168,7 @@ class ConversationViewController: UIViewController,
     @objc func imageTapped() {
         let alert = UIAlertController.init(title: nil,
                                            message: nil,
-                                           preferredStyle: .alert)
+                                           preferredStyle: .actionSheet)
         let pictureAction = UIAlertAction(title: L10n.Alerts.uploadPhoto, style: UIAlertAction.Style.default) {[weak self] _ in
             self?.checkPhotoLibraryPermission()
         }
@@ -224,10 +226,12 @@ class ConversationViewController: UIViewController,
         }
 
         let cancelAction = UIAlertAction(title: L10n.Alerts.profileCancelPhoto, style: UIAlertAction.Style.cancel)
+
         alert.addAction(pictureAction)
         alert.addAction(recordVideoAction)
         alert.addAction(recordAudioAction)
         alert.addAction(documentsAction)
+        alert.addAction(locationSharingAction())
         alert.addAction(cancelAction)
         alert.popoverPresentationController?.sourceView = self.view
         alert.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection()
@@ -638,6 +642,8 @@ class ConversationViewController: UIViewController,
         self.tableView.register(cellType: MessageCellDataTransferSent.self)
         self.tableView.register(cellType: MessageCellDataTransferReceived.self)
         self.tableView.register(cellType: MessageCellGenerated.self)
+        self.tableView.register(cellType: MessageCellLocationSharingSent.self)
+        self.tableView.register(cellType: MessageCellLocationSharingReceived.self)
 
         //Bind the TableView to the ViewModel
         self.viewModel.messages.asObservable()
@@ -667,7 +673,7 @@ class ConversationViewController: UIViewController,
     }
 
     fileprivate func scrollToBottomIfNeed() {
-        if self.isBottomContentOffset && !self.isExecutingDeleteMessage {
+        if (self.isBottomContentOffset || !self.tableView.isScrollEnabled) && !self.isExecutingDeleteMessage {
             self.scrollToBottom(animated: false)
         }
         if self.isExecutingDeleteMessage {
@@ -679,6 +685,7 @@ class ConversationViewController: UIViewController,
         let numberOfRows = self.tableView.numberOfRows(inSection: 0)
         if  numberOfRows > 0 {
             let last = IndexPath(row: numberOfRows - 1, section: 0)
+            self.tableView.isScrollEnabled = true
             self.tableView.scrollToRow(at: last, at: .bottom, animated: animated)
         }
     }
@@ -908,6 +915,7 @@ class ConversationViewController: UIViewController,
     }
 }
 
+// MARK: TableDataSource
 extension ConversationViewController: UITableViewDataSource {
     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         return self.messageViewModels?.count ?? 0
@@ -923,20 +931,35 @@ extension ConversationViewController: UITableViewDataSource {
                                                 messageId: item.message.messageId)
             }
 
-            let cellType = { (bubblePosition: BubblePosition, isTransfer: Bool) -> MessageCell.Type in
+            let cellType = { (bubblePosition: BubblePosition, isTransfer: Bool, isLocationSharing: Bool) -> MessageCell.Type in
                 switch bubblePosition {
-                case .received: return isTransfer ? MessageCellDataTransferReceived.self : MessageCellReceived.self
-                case .sent: return isTransfer ? MessageCellDataTransferSent.self : MessageCellSent.self
+                case .received:
+                    if isLocationSharing {
+                        return MessageCellLocationSharingReceived.self
+                    } else if isTransfer {
+                        return MessageCellDataTransferReceived.self
+                    } else {
+                        return MessageCellReceived.self
+                    }
+                case .sent:
+                    if isLocationSharing {
+                        return MessageCellLocationSharingSent.self
+                    } else if isTransfer {
+                        return MessageCellDataTransferSent.self
+                    } else {
+                        return MessageCellSent.self
+                    }
                 case .generated: return MessageCellGenerated.self
                 }
-            }(item.bubblePosition(), item.isTransfer)
+            }(item.bubblePosition(), item.isTransfer, item.isLocationSharingBubble)
 
             let cell = tableView.dequeueReusableCell(for: indexPath, cellType: cellType)
             cell.configureFromItem(viewModel, self.messageViewModels, cellForRowAt: indexPath)
 
-            transferCellSetup(item, cell, tableView, indexPath)
-            deleteCellSetup(cell)
-            tapToShowTimeCellSetup(cell)
+            self.transferCellSetup(item, cell, tableView, indexPath)
+            self.locationCellSetup(item, cell)
+            self.deleteCellSetup(cell)
+            self.tapToShowTimeCellSetup(cell)
 
             return cell
         }
@@ -948,6 +971,13 @@ extension ConversationViewController: UITableViewDataSource {
             .observeOn(MainScheduler.instance)
             .subscribe(onNext: { [weak self, weak cell] (shouldDelete) in
                 guard shouldDelete, let self = self, let cell = cell, let messageId = cell.messageId else { return }
+
+                if cell as? MessageCellLocationSharing != nil {
+                    self.tableView.isScrollEnabled = true
+                    if cell as? MessageCellLocationSharingSent != nil {
+                        self.viewModel.stopSendingLocation()
+                    }
+                }
                 self.isExecutingDeleteMessage = true
                 self.viewModel.deleteMessage(messageId: messageId)
             })
@@ -972,6 +1002,42 @@ extension ConversationViewController: UITableViewDataSource {
             .disposed(by: cell.disposeBag)
     }
 
+    private func locationCellSetup(_ item: MessageViewModel, _ cell: MessageCell) {
+        guard item.isLocationSharingBubble, let cell = cell as? MessageCellLocationSharing else { return }
+
+        cell.locationTapped
+            .observeOn(MainScheduler.instance)
+            .subscribe(onNext: { [weak self, weak cell] (locationTapped) in
+                guard locationTapped.0, let self = self, let cell = cell else { return }
+
+                let expanding = locationTapped.1
+
+                if let index = self.tableView.indexPath(for: cell) {
+                    cell.expandHeight(expanding,
+                                      self.tableView.frame.height - self.tableView.contentInset.top - self.tableView.contentInset.bottom)
+                    self.tableView.performBatchUpdates({
+                        self.tableView.updateConstraintsIfNeeded()
+                        UIView.animate(withDuration: 0.4) {
+                            cell.updateWidth(expanding)
+                            cell.layoutIfNeeded()
+                        }
+                    }, completion: nil)
+
+                    if expanding {
+                        self.tableView.scrollToRow(at: index, at: UITableView.ScrollPosition.top, animated: true)
+                    }
+                    self.tableView.isScrollEnabled = !expanding
+
+                    cell.locationTapped.accept((false, expanding))
+                } else {
+                    self.log.warning("[ConversationViewController] locationCellSetup, something went weird, let's retry")
+                    self.tableView.isScrollEnabled = true
+                    cell.locationTapped.accept((true, expanding)) // retry
+                }
+            })
+            .disposed(by: cell.disposeBag)
+    }
+
     // swiftlint:disable cyclomatic_complexity
     private func transferCellSetup(_ item: MessageViewModel, _ cell: MessageCell, _ tableView: UITableView, _ indexPath: IndexPath) {
         if item.isTransfer {
@@ -1056,5 +1122,81 @@ extension ConversationViewController: UITableViewDataSource {
         }
     }
 }
+
+// MARK: Location sharing
+extension ConversationViewController {
+    private func locationSharingAction() -> UIAlertAction {
+        return UIAlertAction(title: L10n.Alerts.locationSharing, style: .default) { [weak self] _ in
+            guard let self = self else { return }
+
+            if self.canShareLocation() && self.isNotAlreadySharingWithThisContact() {
+                self.askLocationSharingDuration()
+            }
+        }
+    }
+
+    private func askLocationSharingDuration() {
+        let alert = UIAlertController.init(title: L10n.Alerts.locationSharingDurationTitle,
+                                           message: nil,
+                                           preferredStyle: .alert)
+
+        alert.addAction(.init(title: L10n.Alerts.locationSharingDuration10min, style: .default, handler: { [weak self] _ in
+            self?.viewModel.startSendingLocation(duration: 10 * 60)
+        }))
+        alert.addAction(.init(title: L10n.Alerts.locationSharingDuration1hour, style: .default, handler: { [weak self] _ in
+            self?.viewModel.startSendingLocation(duration: 60 * 60)
+        }))
+        alert.addAction(.init(title: L10n.Alerts.profileCancelPhoto, style: UIAlertAction.Style.cancel))
+
+        self.present(alert, animated: true, completion: nil)
+    }
+
+    private func isNotAlreadySharingWithThisContact() -> Bool {
+        if self.viewModel.isAlreadySharingLocation() {
+            let alert = UIAlertController.init(title: L10n.Alerts.alreadylocationSharing,
+                                               message: nil,
+                                               preferredStyle: .alert)
+            alert.addAction(.init(title: L10n.Global.ok, style: UIAlertAction.Style.cancel))
+            self.present(alert, animated: true, completion: nil)
+
+            return false
+        }
+        return true
+    }
+
+    private func canShareLocation() -> Bool {
+        if CLLocationManager.locationServicesEnabled() {
+            return checkLocationAuthorization()
+        } else {
+            self.showGoToSettingsAlert(title: L10n.Alerts.locationServiceIsDisabled)
+            return false
+        }
+    }
+
+    private func showGoToSettingsAlert(title: String) {
+        let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
+
+        alertController.addAction(UIAlertAction(title: L10n.Actions.goToSettings, style: .default, handler: { (_) in
+            if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
+                UIApplication.shared.open(url, completionHandler: nil)
+            }
+        }))
+
+        alertController.addAction(UIAlertAction(title: L10n.Actions.cancelAction, style: .cancel, handler: nil))
+
+        self.present(alertController, animated: true, completion: nil)
+    }
+
+    private func checkLocationAuthorization() -> Bool {
+        switch CLLocationManager.authorizationStatus() {
+        case .notDetermined: locationManager.requestWhenInUseAuthorization()
+        case .restricted, .denied: self.showGoToSettingsAlert(title: L10n.Alerts.noLocationPermissionsTitle)
+        case .authorizedAlways, .authorizedWhenInUse: return true
+        @unknown default: break
+        }
+
+        return false
+    }
+}
 // swiftlint:enable type_body_length
 // swiftlint:enable file_length
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
index 0181dbf4881a490b3b60290010d556733a1d7017..ffd13c85e89700e4b23819fed477ed43e6d7ae7e 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
@@ -44,6 +44,7 @@ class ConversationViewModel: Stateable, ViewModel {
     private let profileService: ProfilesService
     private let dataTransferService: DataTransferService
     private let callService: CallsService
+    private let locationSharingService: LocationSharingService
 
     private let injectionBag: InjectionBag
 
@@ -97,9 +98,39 @@ class ConversationViewModel: Stateable, ViewModel {
         self.profileService = injectionBag.profileService
         self.dataTransferService = injectionBag.dataTransferService
         self.callService = injectionBag.callService
+        self.locationSharingService = injectionBag.locationSharingService
 
         dateFormatter.dateStyle = .medium
         hourFormatter.dateFormat = "HH:mm"
+
+        self.subscribeLocationReceivedEvent()
+        self.subscribeProfileServiceEvent()
+    }
+
+    private func subscribeLocationReceivedEvent() {
+        self.locationSharingService
+            .peerUriAndLocationReceived
+            .subscribe(onNext: { [weak self] tuple in
+                guard let self = self, let peerUri = tuple.0, let coordinates = tuple.1, let conversation = self.conversation else { return }
+                if peerUri == conversation.value.participantUri {
+                    self.myContactsLocation.onNext(coordinates)
+                }
+            })
+           .disposed(by: self.disposeBag)
+    }
+
+    private func subscribeProfileServiceEvent() {
+        guard let account = self.accountService.currentAccount else { return }
+        self.profileService
+            .getAccountProfile(accountId: account.id)
+            .subscribe(onNext: { [weak self] profile in
+                guard let self = self else { return }
+                if let photo = profile.photo,
+                    let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
+                    self.myOwnProfileImageData = data
+                }
+            })
+            .disposed(by: self.disposeBag)
     }
 
     var conversation: Variable<ConversationModel>! {
@@ -136,12 +167,9 @@ class ConversationViewModel: Stateable, ViewModel {
                 })
                 .observeOn(MainScheduler.instance)
                 .subscribe(onNext: { [weak self] messageViewModels in
-                    guard let self = self else {
-                        return
-                    }
+                    guard let self = self else { return }
                     var msg = messageViewModels
-                    if self
-                        .peerComposingMessage {
+                    if self.peerComposingMessage {
                         let msgModel = MessageModel(withId: "",
                                                     receivedDate: Date(),
                                                     content: "       ",
@@ -283,8 +311,12 @@ class ConversationViewModel: Stateable, ViewModel {
         }
     }()
 
+    // My contact's
     var profileImageData = Variable<Data?>(nil)
 
+    // Mine
+    var myOwnProfileImageData: Data?
+
     var inviteButtonIsAvailable = BehaviorSubject(value: true)
 
     var contactPresence = Variable<Bool>(false)
@@ -655,4 +687,35 @@ class ConversationViewModel: Stateable, ViewModel {
     func isLastDisplayed(messageId: Int64) -> Bool {
         return messageId == self.conversation.value.lastDisplayedMessage.id
     }
+
+    var myLocation: Observable<CLLocation?> { return self.locationSharingService.currentLocation.asObservable() }
+
+    var myContactsLocation = BehaviorSubject<CLLocationCoordinate2D?>(value: nil)
+}
+
+// MARK: Sharing my location
+extension ConversationViewModel {
+
+    func isAlreadySharingLocation() -> Bool {
+        guard let account = self.accountService.currentAccount else { return true }
+        return self.locationSharingService.isAlreadySharing(accountId: account.id,
+                                                            contactUri: self.conversation.value.participantUri)
+    }
+
+    func startSendingLocation(duration: TimeInterval) {
+        if self.conversation.value.messages.isEmpty {
+               self.sendContactRequest()
+        }
+
+        guard let account = self.accountService.currentAccount else { return }
+        self.locationSharingService.startSharingLocation(from: account.id,
+                                                         to: self.conversation.value.participantUri,
+                                                         duration: duration)
+    }
+
+    func stopSendingLocation() {
+        guard let account = self.accountService.currentAccount else { return }
+        self.locationSharingService.stopSharingLocation(accountId: account.id,
+                                                        contactUri: self.conversation.value.participantUri)
+    }
 }
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
index b9e2833b4ba8f7a47b18f5e423e87af6eb4acb32..be60044c69206cd8f723167f448d1fb1335408b0 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
@@ -66,6 +66,8 @@ class MessageViewModel {
     var sequencing: MessageSequencing = .unknown
     var isComposingIndicator: Bool = false
 
+    var isLocationSharingBubble: Bool { return self.message.isLocationSharing }
+
     private let disposeBag = DisposeBag()
     let injectBug: InjectionBag
 
diff --git a/Ring/Ring/Info.plist b/Ring/Ring/Info.plist
index 6c6e947dc7223428460328fcb278e0d2d6a5b81c..4b8ef755707b7307c9e0eb0e844ce98d43a3a598 100644
--- a/Ring/Ring/Info.plist
+++ b/Ring/Ring/Info.plist
@@ -33,6 +33,8 @@
 	</dict>
 	<key>NSCameraUsageDescription</key>
 	<string>Used to create avatar, record video and share photos with contacts</string>
+	<key>NSLocationWhenInUseUsageDescription</key>
+	<string>Used to share your location with your contacts</string>
 	<key>NSMicrophoneUsageDescription</key>
 	<string>The microphone will be used for communication during a call.</string>
 	<key>NSPhotoLibraryAddUsageDescription</key>
@@ -53,9 +55,10 @@
 	</dict>
 	<key>UIBackgroundModes</key>
 	<array>
-		<string>voip</string>
 		<string>fetch</string>
+		<string>location</string>
 		<string>remote-notification</string>
+		<string>voip</string>
 	</array>
 	<key>UIFileSharingEnabled</key>
 	<true/>
diff --git a/Ring/Ring/Models/MessageModel.swift b/Ring/Ring/Models/MessageModel.swift
index b281fe2cc4c859cf1f19f1bb64ee55ad3145d00c..187daf34b77bd3442576efb8c30993c3aabeecee 100644
--- a/Ring/Ring/Models/MessageModel.swift
+++ b/Ring/Ring/Models/MessageModel.swift
@@ -30,6 +30,7 @@ class MessageModel {
     var isGenerated: Bool = false
     var isTransfer: Bool = false
     var incoming: Bool
+    var isLocationSharing: Bool = false
 
     init(withId id: String, receivedDate: Date, content: String, authorURI: String, incoming: Bool) {
         self.daemonId = id
diff --git a/Ring/Ring/Resources/Images.xcassets/my_location.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/Contents.json
new file mode 100644
index 0000000000000000000000000000000000000000..bc2011f2788256a72570eec5eeaa28391eba98d7
--- /dev/null
+++ b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+  "images" : [
+    {
+      "filename" : "my_location.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "filename" : "my_location_2x.png",
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "filename" : "my_location_3x.png",
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location.png b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location.png
new file mode 100644
index 0000000000000000000000000000000000000000..c6904f2bd3f3af553f37970d0f3f78af2c4fc260
Binary files /dev/null and b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location.png differ
diff --git a/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_2x.png b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..ac4587fec7bc2475f16739efa6cd95157b753773
Binary files /dev/null and b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_2x.png differ
diff --git a/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_3x.png b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..51fa8dae767b7d13d2d3877c280cd9dac85ebc83
Binary files /dev/null and b/Ring/Ring/Resources/Images.xcassets/my_location.imageset/my_location_3x.png differ
diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings
index 331546d490ace4c6ba7f1d747283abd408c18e0c..44c4e21bfbdca93e03884faec83ba6ca226b5150 100644
--- a/Ring/Ring/Resources/en.lproj/Localizable.strings
+++ b/Ring/Ring/Resources/en.lproj/Localizable.strings
@@ -132,11 +132,21 @@
 "alerts.confirmClearConversationTitle" = "Clear Conversation";
 "alerts.noMediaPermissionsTitle" = "Media permission not granted";
 "alerts.noLibraryPermissionsTitle" = "Access to photo library not granted";
+"alerts.noLocationPermissionsTitle" = "Access to location not granted";
 "alerts.errorWrongCredentials" = "Cannot connect to provided account manager. Please check your credentials";
 "alerts.recordVideoMessage" = "Record a video message";
 "alerts.recordAudioMessage" = "Record an audio message";
 "alerts.uploadFile" = "Upload file";
 "alerts.uploadPhoto" = "Upload photo or movie";
+"alerts.locationServiceIsDisabled" = "Turn on \"Location Services\" to allow \"Jami\" to determine your location.";
+"alerts.locationSharing" = "Share my location";
+"alerts.alreadylocationSharing" = "Already sharing location with this user";
+"alerts.locationSharingDurationTitle" = "How long should the location sharing be?";
+"alerts.locationSharingDuration10min" = "10 min";
+"alerts.locationSharingDuration1hour" = "1 hour";
+"alerts.mapInformation" = "Map information";
+"alerts.openStreetMapCopyright" = "© OpenStreetMap contributors";
+"alerts.openStreetMapCopyrightMoreInfo" = "Learn more";
 
 //Actions
 "actions.blockAction" = "Block";
@@ -150,6 +160,8 @@
 "alerts.incomingCallButtonIgnore" = "Ignore";
 "actions.startAudioCall" = "  Audio Call";
 "actions.startVideoCall" = "  Video Call";
+"actions.goToSettings" = "Go to Settings";
+"actions.stopLocationSharing" = "Stop sharing";
 
 //Calls
 "calls.callItemTitle" = "Call";
@@ -277,6 +289,8 @@
 "notifications.acceptCall" = "ACCEPT";
 "notifications.refuseCall" = "REFUSE";
 "notifications.newFile" = "New file";
+"notifications.locationSharingStarted" = "Incoming location sharing started";
+"notifications.locationSharingStopped" = "Incoming location sharing stopped";
 "dataTransfer.readableStatusAwaiting" = "Pending…";
 "dataTransfer.readableStatusRefuse" = "Refuse";
 "dataTransfer.readableStatusOngoing" = "Transferring";
@@ -296,6 +310,7 @@
 "generatedMessage.incomingCall" = "Incoming call";
 "generatedMessage.missedOutgoingCall" = "Missed outgoing call";
 "generatedMessage.missedIncomingCall" = "Missed incoming call";
+"generatedMessage.liveLocationSharing" = "Live location sharing";
 
 //General Settings
 "generalSettings.title" = "General settings";
diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift
index e2570bd4c77fe9c613c5aac7ec647ddde6a94629..dc3f27de101e64c0dca12ed0a762b8b50b4767a2 100644
--- a/Ring/Ring/Services/AccountsService.swift
+++ b/Ring/Ring/Services/AccountsService.swift
@@ -221,13 +221,18 @@ class AccountsService: AccountAdapterDelegate {
         return true
     }
 
+    /// This function clears the temporary database entries
+    private func sanitizeDatabases() -> Bool {
+        let accountIds = self.accountList.map({ $0.id })
+        return self.dbManager.deleteAllLocationUpdates(accountIds: accountIds)
+    }
+
     func initialAccountsLoading() -> Completable {
         return Completable.create { [unowned self] completable in
             self.loadAccountsFromDaemon()
             if self.accountList.isEmpty {
                 completable(.completed)
-            }
-            if self.loadDatabases() {
+            } else if self.loadDatabases() && self.sanitizeDatabases() {
                 completable(.completed)
             } else {
                 completable(.error(DataAccessError.databaseError))
diff --git a/Ring/Ring/Services/ConversationsManager.swift b/Ring/Ring/Services/ConversationsManager.swift
index 11041a06e3f1c208d38ef3678f2e7e80d375c280..058a9fa2994fe46b5ba8a15c456b0a28afd12468 100644
--- a/Ring/Ring/Services/ConversationsManager.swift
+++ b/Ring/Ring/Services/ConversationsManager.swift
@@ -1,7 +1,8 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -22,34 +23,111 @@ import Foundation
 import RxSwift
 import SwiftyBeaver
 
+// swiftlint:disable type_body_length
 class ConversationsManager: MessagesAdapterDelegate {
 
     let log = SwiftyBeaver.self
 
-    let conversationService: ConversationsService
-    let accountsService: AccountsService
-    let nameService: NameService
-    let dataTransferService: DataTransferService
-    let callService: CallsService
+    private let conversationService: ConversationsService
+    private let accountsService: AccountsService
+    private let nameService: NameService
+    private let dataTransferService: DataTransferService
+    private let callService: CallsService
+    private let locationSharingService: LocationSharingService
 
     private let disposeBag = DisposeBag()
     fileprivate let textPlainMIMEType = "text/plain"
+    private let geoLocationMIMEType = "application/geo"
     fileprivate let maxSizeForAutoaccept = 20 * 1024 * 1024
     private let notificationHandler = LocalNotificationsHelper()
 
     // swiftlint:disable cyclomatic_complexity
-    init(with conversationService: ConversationsService, accountsService: AccountsService, nameService: NameService, dataTransferService: DataTransferService, callService: CallsService) {
+    init(with conversationService: ConversationsService, accountsService: AccountsService, nameService: NameService,
+         dataTransferService: DataTransferService, callService: CallsService, locationSharingService: LocationSharingService) {
         self.conversationService = conversationService
         self.accountsService = accountsService
         self.nameService = nameService
         self.dataTransferService = dataTransferService
         self.callService = callService
+        self.locationSharingService = locationSharingService
+
         MessagesAdapter.delegate = self
-        subscribeFileTransferEvents()
-        subscribeCallsEvents()
+        self.subscribeFileTransferEvents()
+        self.subscribeCallsEvents()
+        self.subscribeLocationSharingEvent()
+    }
+
+    private func subscribeLocationSharingEvent() {
+        self.locationSharingService
+            .locationServiceEventShared
+            .filter({ $0.eventType == ServiceEventType.stopLocationSharing })
+            .subscribe(onNext: { [weak self] event in
+                guard let self = self else { return }
+                var data = [String: String]()
+                data[NotificationUserInfoKeys.messageContent.rawValue] = event.getEventInput(ServiceEventInput.content)
+                data[NotificationUserInfoKeys.participantID.rawValue] = event.getEventInput(ServiceEventInput.peerUri)
+                data[NotificationUserInfoKeys.accountID.rawValue] =  event.getEventInput(ServiceEventInput.accountId)
+
+                guard let contactUri = data[NotificationUserInfoKeys.participantID.rawValue],
+                      let hash = JamiURI(schema: URIType.ring, infoHach: contactUri).hash else { return }
+
+                DispatchQueue.main.async { [unowned self] in
+                    self.searchNameAndPresentNotification(data: data, hash: hash)
+                }
+            })
+            .disposed(by: self.disposeBag)
+
+        self.locationSharingService
+            .locationServiceEventShared
+            .filter({ $0.eventType == ServiceEventType.sendLocation })
+            .subscribe(onNext: { [weak self] event in
+                guard let self = self,
+                      let currentAccount = self.accountsService.currentAccount,
+                      let (content, shouldTryToSave): (String, Bool) = event.getEventInput(ServiceEventInput.content),
+                      let accountId: String = event.getEventInput(ServiceEventInput.accountId),
+                      let account = self.accountsService.getAccount(fromAccountId: accountId),
+                      let peerUri: String = event.getEventInput(ServiceEventInput.peerUri)
+                      else { return }
+
+                let shouldRefresh = currentAccount.id == accountId
+
+                self.conversationService
+                    .sendLocation(withContent: content,
+                                  from: account,
+                                  recipientUri: peerUri,
+                                  shouldRefreshConversations: shouldRefresh,
+                                  shouldTryToSave: shouldTryToSave)
+                    .subscribe(onCompleted: { [weak self] in
+                        self?.log.debug("[LocationSharingService] Location sent")
+                    }).disposed(by: self.disposeBag)
+            })
+            .disposed(by: self.disposeBag)
+
+        self.locationSharingService
+            .locationServiceEventShared
+            .filter({ $0.eventType == ServiceEventType.deleteLocation })
+            .subscribe(onNext: { [weak self] event in
+                guard let self = self,
+                    let currentAccount = self.accountsService.currentAccount,
+                    let (incoming, shouldRefreshConversations): (Bool, Bool) = event.getEventInput(ServiceEventInput.content),
+                    let accountId: String = event.getEventInput(ServiceEventInput.accountId),
+                    let peerUri: String = event.getEventInput(ServiceEventInput.peerUri)
+                    else { return }
+
+                let shouldRefresh = currentAccount.id == accountId && shouldRefreshConversations
+
+                self.conversationService.deleteLocationUpdate(incoming: incoming,
+                                                              peerUri: peerUri,
+                                                              accountId: accountId,
+                                                              shouldRefreshConversations: shouldRefresh)
+                    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
+                    .subscribe()
+                    .disposed(by: self.disposeBag)
+            })
+            .disposed(by: self.disposeBag)
     }
 
-    func subscribeCallsEvents() {
+    private func subscribeCallsEvents() {
         self.callService.newMessage.filter({ (event) in
             return  event.eventType == ServiceEventType.newIncomingMessage
         })
@@ -95,7 +173,7 @@ class ConversationsManager: MessagesAdapterDelegate {
             })
             .disposed(by: disposeBag)
     }
-    func subscribeFileTransferEvents() {
+    private func subscribeFileTransferEvents() {
         self.dataTransferService
             .sharedResponseStream
             .filter({ (event) in
@@ -173,25 +251,73 @@ class ConversationsManager: MessagesAdapterDelegate {
         if self.accountsService.boothMode() {
             return
         }
-        guard let content = message[textPlainMIMEType] else {
-            return
+        if let content = message[textPlainMIMEType] {
+            DispatchQueue.main.async { [unowned self] in
+                self.handleNewMessage(from: senderAccount,
+                                      to: receiverAccountId,
+                                      messageId: messageId,
+                                      message: content,
+                                      peerName: nil)
+            }
+        } else if let content = message[geoLocationMIMEType] {
+            DispatchQueue.main.async { [unowned self] in
+                self.handleReceivedLocationUpdate(from: senderAccount,
+                                                  to: receiverAccountId,
+                                                  messageId: messageId,
+                                                  locationJSON: content)
+            }
         }
-        DispatchQueue.main.async { [unowned self] in
-            self.handleNewMessage(from: senderAccount,
-                                  to: receiverAccountId,
-                                  messageId: messageId,
-                                  message: content,
-                                  peerName: nil)
+    }
+
+    private func handleReceivedLocationUpdate(from peerUri: String, to accountId: String, messageId: String, locationJSON content: String) {
+        guard let currentAccount = self.accountsService.currentAccount,
+              let accountForMessage = self.accountsService.getAccount(fromAccountId: accountId) else { return }
+
+        let type = AccountModelHelper.init(withAccount: accountForMessage).isAccountSip() ? URIType.sip : URIType.ring
+        guard let peerUri = JamiURI.init(schema: type, infoHach: peerUri, account: accountForMessage).uriString else {return}
+
+        if self.conversationService.isBeginningOfLocationSharing(incoming: true, contactUri: peerUri, accountId: accountId) {
+            // Handle notification
+            if UIApplication.shared.applicationState != .active && AccountModelHelper
+                .init(withAccount: accountForMessage).isAccountRing() &&
+                accountsService.getCurrentProxyState(accountID: accountId) {
+                var data = [String: String]()
+                data [NotificationUserInfoKeys.messageContent.rawValue] = L10n.Notifications.locationSharingStarted
+                data [NotificationUserInfoKeys.participantID.rawValue] = peerUri
+                data [NotificationUserInfoKeys.accountID.rawValue] = accountId
+
+                // only for jami accounts
+                if let hash = JamiURI(schema: URIType.ring, infoHach: peerUri).hash {
+                    self.searchNameAndPresentNotification(data: data, hash: hash)
+                }
+            }
+
+            let shouldRefresh = currentAccount.id == accountId
+
+            // Save (if first)
+            guard let uriString = JamiURI.init(schema: type,
+                                               infoHach: peerUri,
+                                               account: accountForMessage).uriString else { return }
+            let message = self.conversationService.createLocation(withId: messageId,
+                                                                  byAuthor: uriString,
+                                                                  incoming: true)
+            self.conversationService.saveLocation(message: message,
+                                                  toConversationWith: uriString,
+                                                  toAccountId: accountId,
+                                                  shouldRefreshConversations: shouldRefresh,
+                                                  contactUri: peerUri)
+                .subscribe()
+                .disposed(by: self.disposeBag)
         }
+
+        // Tell the location sharing service
+        self.locationSharingService.handleReceivedLocationUpdate(from: peerUri, to: accountId, messageId: messageId, locationJSON: content)
     }
 
     func handleNewMessage(from peerUri: String, to accountId: String, messageId: String, message content: String, peerName: String?) {
-        guard let currentAccount = self.accountsService.currentAccount else {
-            return
-        }
-        guard let accountForMessage = self.accountsService.getAccount(fromAccountId: accountId) else {
-            return
-        }
+        guard let currentAccount = self.accountsService.currentAccount,
+              let accountForMessage = self.accountsService.getAccount(fromAccountId: accountId) else { return }
+
         if UIApplication.shared.applicationState != .active && AccountModelHelper
             .init(withAccount: accountForMessage).isAccountRing() &&
             accountsService.getCurrentProxyState(accountID: accountId) {
@@ -324,3 +450,4 @@ class ConversationsManager: MessagesAdapterDelegate {
         conversationService.detectingMessageTyping(from, for: accountId, status: status)
     }
 }
+// swiftlint:enable type_body_length
diff --git a/Ring/Ring/Services/ConversationsService.swift b/Ring/Ring/Services/ConversationsService.swift
index 73a2e7c08f6da53e1b0db6873343b56a54de7807..6b6af5eb35a461ffbc8e4a421e24f1544b8dc1d3 100644
--- a/Ring/Ring/Services/ConversationsService.swift
+++ b/Ring/Ring/Services/ConversationsService.swift
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ *  Copyright (C) 2017-2020 Savoir-faire Linux Inc.
  *
  *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
  *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -34,6 +34,7 @@ class ConversationsService {
     fileprivate let messageAdapter: MessagesAdapter
     fileprivate let disposeBag = DisposeBag()
     fileprivate let textPlainMIMEType = "text/plain"
+    private let geoLocationMIMEType = "application/geo"
 
     fileprivate let responseStream = PublishSubject<ServiceEvent>()
     var sharedResponseStream: Observable<ServiceEvent>
@@ -158,6 +159,16 @@ class ConversationsService {
                      toConversationWith recipientRingId: String,
                      toAccountId: String,
                      shouldRefreshConversations: Bool) -> Completable {
+        return self.saveMessageModel(message: message, toConversationWith: recipientRingId,
+                                     toAccountId: toAccountId, shouldRefreshConversations: shouldRefreshConversations,
+                                     interactionType: InteractionType.text)
+    }
+
+    func saveMessageModel(message: MessageModel,
+                          toConversationWith recipientRingId: String,
+                          toAccountId: String,
+                          shouldRefreshConversations: Bool,
+                          interactionType: InteractionType = InteractionType.text) -> Completable {
 
         return Completable.create(subscribe: { [unowned self] completable in
             self.messagesSemaphore.wait()
@@ -165,7 +176,7 @@ class ConversationsService {
                                        with: recipientRingId,
                                        message: message,
                                        incoming: message.incoming,
-                                       interactionType: InteractionType.text, duration: 0)
+                                       interactionType: interactionType, duration: 0)
                 .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
                 .subscribe(onNext: { [unowned self] _ in
                     // append new message so it can be found if a status update is received before the DB finishes reload
@@ -500,3 +511,90 @@ class ConversationsService {
         self.responseStream.onNext(serviceEvent)
     }
 }
+
+// MARK: Location
+extension ConversationsService {
+
+    func createLocation(withId messageId: String, byAuthor author: String, incoming: Bool) -> MessageModel {
+        return MessageModel(withId: messageId, receivedDate: Date(), content: L10n.GeneratedMessage.liveLocationSharing, authorURI: author, incoming: incoming)
+    }
+
+    // TODO: Possible extraction with sendMessage
+    func sendLocation(withContent content: String, from senderAccount: AccountModel,
+                      recipientUri: String, shouldRefreshConversations: Bool,
+                      shouldTryToSave: Bool) -> Completable {
+
+        return Completable.create(subscribe: { [unowned self] completable in
+            let contentDict = [self.geoLocationMIMEType: content]
+            let messageId = String(self.messageAdapter.sendMessage(withContent: contentDict, withAccountId: senderAccount.id, to: recipientUri))
+            let accountHelper = AccountModelHelper(withAccount: senderAccount)
+            let type = accountHelper.isAccountSip() ? URIType.sip : URIType.ring
+            let contactUri = JamiURI.init(schema: type, infoHach: recipientUri, account: senderAccount)
+            guard let stringUri = contactUri.uriString else {
+                completable(.completed)
+                return Disposables.create {}
+            }
+            if shouldTryToSave, let uri = accountHelper.uri, uri != recipientUri {
+                let message = self.createLocation(withId: messageId,
+                                                  byAuthor: uri,
+                                                  incoming: false)
+                self.saveLocation(message: message,
+                                  toConversationWith: stringUri,
+                                  toAccountId: senderAccount.id,
+                                  shouldRefreshConversations: shouldRefreshConversations,
+                                  contactUri: recipientUri)
+                    .subscribe(onCompleted: { [unowned self] in
+                        self.log.debug("Location saved")
+                    })
+                    .disposed(by: self.disposeBag)
+            }
+            completable(.completed)
+            return Disposables.create {}
+        })
+    }
+
+    // Save location only if it's the first one
+    func isBeginningOfLocationSharing(incoming: Bool, contactUri: String, accountId: String) -> Bool {
+        let isFirstLocationIncomingUpdate = self.dbManager.isFirstLocationIncomingUpdate(incoming: incoming, peerUri: contactUri, accountId: accountId)
+        return isFirstLocationIncomingUpdate != nil && isFirstLocationIncomingUpdate!
+    }
+
+    // Location saved doesn't actually contain the geolocation data
+    func saveLocation(message: MessageModel,
+                      toConversationWith recipientRingId: String,
+                      toAccountId: String,
+                      shouldRefreshConversations: Bool,
+                      contactUri: String) -> Completable {
+        if self.isBeginningOfLocationSharing(incoming: message.incoming, contactUri: contactUri, accountId: toAccountId) {
+            return self.saveMessageModel(message: message, toConversationWith: recipientRingId,
+                                         toAccountId: toAccountId, shouldRefreshConversations: shouldRefreshConversations,
+                                         interactionType: InteractionType.location)
+        }
+        return Completable.create(subscribe: { completable in
+            completable(.completed)
+            return Disposables.create { }
+        })
+    }
+
+    func deleteLocationUpdate(incoming: Bool, peerUri: String, accountId: String, shouldRefreshConversations: Bool) -> Completable {
+        return Completable.create(subscribe: { [unowned self] completable in
+            self.dbManager.deleteLocationUpdates(incoming: incoming, peerUri: peerUri, accountId: accountId)
+                .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
+                .subscribe(onCompleted: {
+                    if shouldRefreshConversations {
+                        self.dbManager.getConversationsObservable(for: accountId)
+                            .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
+                            .subscribe(onNext: { [unowned self] conversationsModels in
+                                self.conversations.value = conversationsModels
+                            })
+                            .disposed(by: (self.disposeBag))
+                    }
+                    completable(.completed)
+                }, onError: { (error) in
+                    completable(.error(error))
+                })
+                .disposed(by: self.disposeBag)
+            return Disposables.create { }
+        })
+    }
+}
diff --git a/Ring/Ring/Services/LocationSharingService.swift b/Ring/Ring/Services/LocationSharingService.swift
new file mode 100644
index 0000000000000000000000000000000000000000..363c7edaecd67034f05ec8aacc80634bddfdaa23
--- /dev/null
+++ b/Ring/Ring/Services/LocationSharingService.swift
@@ -0,0 +1,356 @@
+/*
+*  Copyright (C) 2020 Savoir-faire Linux Inc.
+*
+*  Author: Raphaël Brulé <raphael.brule@savoirfairelinux.com>
+*
+*  This program is free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 3 of the License, or
+*  (at your option) any later version.
+*
+*  This program is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  You should have received a copy of the GNU General Public License
+*  along with this program; if not, write to the Free Software
+*  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+*/
+
+import RxSwift
+import RxCocoa
+import SwiftyBeaver
+
+// swiftlint:disable redundant_string_enum_value
+enum SerializableLocationTypes: String {
+    case position = "position"
+    case stop = "stop"
+}
+// swiftlint:enable redundant_string_enum_value
+
+struct SerializableLocation: Codable {
+    var type: String?   //position (optional) and stop
+    var lat: Double?    //position
+    var long: Double?   //position
+    var alt: Double?    //position
+    var time: Int64     //position and stop
+    var bearing: Float? //position (optional)
+    var speed: Float?   //position (optional)
+}
+
+private class LocationSharingInstanceDictionary<T: LocationSharingInstance> {
+    private var instances: [String: T] = [:]
+
+    var isEmpty: Bool { return self.instances.isEmpty }
+
+    private func key(_ accountId: String, _ contactUri: String) -> String {
+        return accountId + contactUri
+    }
+
+    func get(_ accountId: String, _ contactUri: String) -> T? {
+        return self.instances[key(accountId, contactUri)]
+    }
+
+    func insertOrUpdate(_ instance: T) {
+        self.instances[key(instance.accountId, instance.contactUri)] = instance
+    }
+
+    func remove(_ accountId: String, _ contactUri: String) -> T? {
+        return self.instances.removeValue(forKey: key(accountId, contactUri))
+    }
+
+    func asArray() -> [T] {
+        return self.instances.map({ $0.value })
+    }
+}
+
+private class LocationSharingInstance {
+    let accountId: String
+    let contactUri: String
+
+    init(accountId: String, contactUri: String) {
+        self.accountId = accountId
+        self.contactUri = contactUri
+    }
+}
+
+private class OutgoingLocationSharingInstance: LocationSharingInstance {
+
+    private let locationSharingService: LocationSharingService
+    let duration: TimeInterval
+
+    private var endSharingTimer: Timer?
+
+    init(locationSharingService: LocationSharingService, accountId: String, contactUri: String, duration: TimeInterval) {
+        self.locationSharingService = locationSharingService
+        self.duration = duration
+        super.init(accountId: accountId, contactUri: contactUri)
+
+        self.endSharingTimer =
+            Timer.scheduledTimer(timeInterval: self.duration,
+                                 target: self,
+                                 selector: #selector(self.endSharing),
+                                 userInfo: nil,
+                                 repeats: false)
+    }
+
+    @objc private func endSharing(timer: Timer) {
+        self.locationSharingService.stopSharingLocation(accountId: self.accountId, contactUri: self.contactUri)
+    }
+
+    func invalidate() {
+        if let timer = self.endSharingTimer {
+            timer.invalidate()
+            self.endSharingTimer = nil
+        }
+    }
+}
+
+private class IncomingLocationSharingInstance: LocationSharingInstance {
+
+    var lastReceivedDate: Date
+    var lastReceivedTimeStamp: Int64
+
+    init(accountId: String, contactUri: String, lastReceivedDate: Date, lastReceivedTimeStamp: Int64) {
+        self.lastReceivedDate = lastReceivedDate
+        self.lastReceivedTimeStamp = lastReceivedTimeStamp
+        super.init(accountId: accountId, contactUri: contactUri)
+    }
+}
+
+class LocationSharingService: NSObject {
+
+    private let incomingLocationSharingEndingDelay: TimeInterval = 10 * 60 // 10 mins
+
+    private let log = SwiftyBeaver.self
+
+    private let dbManager: DBManager
+
+    private let disposeBag = DisposeBag()
+    private let locationManager = CLLocationManager()
+
+    // Sharing my location
+    let currentLocation = BehaviorRelay<CLLocation?>(value: nil)
+    private let outgoingInstances = LocationSharingInstanceDictionary<OutgoingLocationSharingInstance>()
+
+    // Receiving my contact's location
+    let peerUriAndLocationReceived = BehaviorRelay<(String?, CLLocationCoordinate2D?)>(value: (nil, nil))
+    private let incomingInstances = LocationSharingInstanceDictionary<IncomingLocationSharingInstance>()
+
+    var receivingService: Disposable?
+
+    // ServiceEvents
+    private let locationServiceEventStream = PublishSubject<ServiceEvent>()
+    let locationServiceEventShared: Observable<ServiceEvent>
+
+    init(dbManager: DBManager) {
+        self.dbManager = dbManager
+
+        self.locationServiceEventStream.disposed(by: self.disposeBag)
+        self.locationServiceEventShared = self.locationServiceEventStream.share()
+        super.init()
+
+        self.locationManager.delegate = self
+        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
+        self.locationManager.allowsBackgroundLocationUpdates = true
+        self.initialize()
+    }
+
+    private func initialize() {
+        self.currentLocation
+            .throttle(10, scheduler: MainScheduler.instance)
+            .subscribe(onNext: { [weak self] location in
+                guard let self = self, let location = location else { return }
+                self.doShareLocationAction(location)
+            })
+        .disposed(by: self.disposeBag)
+    }
+
+    private static func serializeLocation(location: SerializableLocation) -> String? {
+        do {
+            let data = try JSONEncoder().encode(location)
+            return String(data: data, encoding: .utf8)!
+        } catch {
+            return nil
+        }
+    }
+
+    private static func deserializeLocation(json: String) -> SerializableLocation? {
+        do {
+            return try JSONDecoder().decode(SerializableLocation.self, from: json.data(using: .utf8)!)
+        } catch {
+            return nil
+        }
+    }
+
+    private func triggerSendLocation(accountId: String, peerUri: String, content: String, shouldTryToSave: Bool) {
+        var event = ServiceEvent(withEventType: .sendLocation)
+        event.addEventInput(.accountId, value: accountId)
+        event.addEventInput(.peerUri, value: peerUri)
+        event.addEventInput(.content, value: (content, shouldTryToSave))
+        self.locationServiceEventStream.onNext(event)
+    }
+
+    private func triggerDeleteLocation(accountId: String, peerUri: String, incoming: Bool, shouldRefreshConversations: Bool) {
+         var event = ServiceEvent(withEventType: .deleteLocation)
+         event.addEventInput(.accountId, value: accountId)
+         event.addEventInput(.peerUri, value: peerUri)
+         event.addEventInput(.content, value: (incoming, shouldRefreshConversations))
+         self.locationServiceEventStream.onNext(event)
+     }
+
+    private func triggerStopSharing(accountId: String, peerUri: String, content: String) {
+        var event = ServiceEvent(withEventType: .stopLocationSharing)
+        event.addEventInput(.accountId, value: accountId)
+        event.addEventInput(.peerUri, value: peerUri)
+        event.addEventInput(.content, value: content)
+        self.locationServiceEventStream.onNext(event)
+    }
+}
+
+// MARK: Sharing my location
+extension LocationSharingService {
+
+    func isAlreadySharing(accountId: String, contactUri: String) -> Bool {
+        return self.outgoingInstances.get(accountId, contactUri) != nil
+    }
+
+    func startSharingLocation(from accountId: String, to recipientUri: String, duration: TimeInterval) {
+        guard !self.isAlreadySharing(accountId: accountId, contactUri: recipientUri) else { return }
+
+        let instanceToInsert = OutgoingLocationSharingInstance(locationSharingService: self,
+                                                               accountId: accountId,
+                                                               contactUri: recipientUri,
+                                                               duration: duration)
+        self.outgoingInstances.insertOrUpdate(instanceToInsert)
+
+        self.locationManager.startUpdatingLocation()
+    }
+
+    private func doShareLocationAction(_ location: CLLocation) {
+        let serializable = SerializableLocation(type: SerializableLocationTypes.position.rawValue,
+                                                lat: location.coordinate.latitude,
+                                                long: location.coordinate.longitude,
+                                                alt: location.altitude,
+                                                time: Int64(Date().timeIntervalSince1970))
+        guard let jsonLocation = LocationSharingService.serializeLocation(location: serializable) else { return }
+
+        for instance in outgoingInstances.asArray() {
+            self.triggerSendLocation(accountId: instance.accountId,
+                                     peerUri: instance.contactUri,
+                                     content: jsonLocation,
+                                     shouldTryToSave: true)
+        }
+    }
+
+    func stopSharingLocation(accountId: String, contactUri: String) {
+        self.outgoingInstances.get(accountId, contactUri)?.invalidate()
+        _ = self.outgoingInstances.remove(accountId, contactUri)
+
+        if self.outgoingInstances.isEmpty {
+            self.locationManager.stopUpdatingLocation()
+        }
+
+        self.triggerDeleteLocation(accountId: accountId, peerUri: contactUri, incoming: false, shouldRefreshConversations: true)
+
+        self.sendStopSharingLocationMessage(from: accountId, to: contactUri)
+    }
+
+    private func sendStopSharingLocationMessage(from accountId: String, to contactUri: String) {
+        let serializable = SerializableLocation(type: SerializableLocationTypes.stop.rawValue,
+                                                time: Int64(Date().timeIntervalSince1970))
+        guard let jsonLocation = LocationSharingService.serializeLocation(location: serializable) else { return }
+
+        self.triggerSendLocation(accountId: accountId,
+                                 peerUri: contactUri,
+                                 content: jsonLocation,
+                                 shouldTryToSave: false)
+    }
+}
+
+// MARK: Receiving my contact's location
+extension LocationSharingService {
+
+    func handleReceivedLocationUpdate(from peerUri: String, to accountId: String, messageId: String, locationJSON content: String) {
+        guard let incomingData = LocationSharingService.deserializeLocation(json: content) else { return }
+
+        if incomingInstances.isEmpty {
+            self.startReceivingService()
+        }
+
+        if let incomingInstance = self.incomingInstances.get(accountId, peerUri) {
+            if incomingInstance.lastReceivedTimeStamp < incomingData.time {
+                incomingInstance.lastReceivedDate = Date()
+                incomingInstance.lastReceivedTimeStamp = incomingData.time
+            } else {
+                return // ignore messages older than the newest we have (when receiving not in order)
+            }
+        } else {
+            self.incomingInstances.insertOrUpdate(IncomingLocationSharingInstance(accountId: accountId,
+                                                                                  contactUri: peerUri,
+                                                                                  lastReceivedDate: Date(),
+                                                                                  lastReceivedTimeStamp: incomingData.time))
+        }
+
+        if incomingData.type == nil || incomingData.type == SerializableLocationTypes.position.rawValue {
+            // TODO: altitude?
+            let peerUriAndData = (peerUri, CLLocationCoordinate2D(latitude: incomingData.lat!, longitude: incomingData.long!))
+            self.peerUriAndLocationReceived.accept(peerUriAndData)
+
+        } else if incomingData.type == SerializableLocationTypes.stop.rawValue {
+            self.stopReceivingLocation(accountId: accountId, contactUri: peerUri)
+        }
+    }
+
+    func stopReceivingLocation(accountId: String, contactUri: String) {
+        self.triggerDeleteLocation(accountId: accountId, peerUri: contactUri, incoming: true, shouldRefreshConversations: true)
+
+        _ = self.incomingInstances.remove(accountId, contactUri)
+
+        if incomingInstances.isEmpty {
+            self.stopReceivingService()
+        }
+
+        self.triggerStopSharing(accountId: accountId, peerUri: contactUri, content: L10n.Notifications.locationSharingStopped)
+    }
+
+    func startReceivingService() {
+        self.stopReceivingService()
+        self.receivingService = Observable<Int>.interval(60, scheduler: MainScheduler.instance)
+            .subscribe({ [weak self] _ in
+                guard let self = self else { return }
+
+                for (instance) in self.incomingInstances.asArray() {
+                    let positiveTimeElapsed = -instance.lastReceivedDate.timeIntervalSinceNow
+                    if positiveTimeElapsed > self.incomingLocationSharingEndingDelay {
+                        self.stopReceivingLocation(accountId: instance.accountId, contactUri: instance.contactUri)
+                    }
+                }
+            })
+    }
+
+    func stopReceivingService() {
+        self.receivingService?.dispose()
+        self.receivingService = nil
+    }
+}
+
+// MARK: CLLocationManagerDelegate
+extension LocationSharingService: CLLocationManagerDelegate {
+    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+        guard let location = locations.last else { return }
+        self.currentLocation.accept(location)
+     }
+    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+        self.log.debug("[LocationSharingService] didFailWithError: \(error)")
+    }
+
+    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
+        if status == .notDetermined || status == .denied || status == .restricted {
+            for instance in outgoingInstances.asArray() {
+                self.stopSharingLocation(accountId: instance.accountId, contactUri: instance.contactUri)
+            }
+        }
+    }
+}
diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift
index eb7132cf1ec08e5ae4b56a825480acc67fcee06f..53d1c96ce551872de5c91150a986deba5b3757be 100644
--- a/Ring/Ring/Services/ServiceEvent.swift
+++ b/Ring/Ring/Services/ServiceEvent.swift
@@ -52,6 +52,9 @@ enum ServiceEventType {
     case migrationEnded
     case lastDisplayedMessageUpdated
     case presenseSubscribed
+    case sendLocation
+    case deleteLocation
+    case stopLocationSharing
 }
 
 /**
diff --git a/Ring/WhirlyGlobeMaply b/Ring/WhirlyGlobeMaply
new file mode 160000
index 0000000000000000000000000000000000000000..2c6c03d9b5c3846cb7c4aa8269b878664e02d4e0
--- /dev/null
+++ b/Ring/WhirlyGlobeMaply
@@ -0,0 +1 @@
+Subproject commit 2c6c03d9b5c3846cb7c4aa8269b878664e02d4e0
diff --git a/Ring/fetch-dependencies.sh b/Ring/fetch-dependencies.sh
new file mode 100755
index 0000000000000000000000000000000000000000..33c5d20726456ef0f038492b4c13fd9a033145e6
--- /dev/null
+++ b/Ring/fetch-dependencies.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+####################################
+## DOWNLOAD CARTHAGE DEPENDENCIES ##
+####################################
+
+# Bootstrap Carthage
+carthage bootstrap --platform iOS --no-use-binaries --cache-builds
+
+############################################
+## DOWNLOAD WHIRLYGLOBEMAPLY DEPENDENCIES ##
+############################################
+
+cd WhirlyGlobeMaply
+git submodule init
+git submodule update
diff --git a/Ring/swiftgen/swiftgen.sh b/Ring/swiftgen/swiftgen.sh
index 93b5fc6fff40e254a598c7f71acb4f7a88b61cc2..36fa0b31b27ee686bb5ea21c79fc5ce96f5776f9 100755
--- a/Ring/swiftgen/swiftgen.sh
+++ b/Ring/swiftgen/swiftgen.sh
@@ -8,9 +8,9 @@ run_swiftgen() {
 	TPLDIR=$(dirname $0)
 
 	echo "SwiftGen: Generating files..."
-	swiftgen storyboards "$SRCDIR" -t swift5 --output "$OUTDIR/Storyboards.swift"
-	swiftgen xcassets "$SRCDIR/Resources/Images.xcassets" -t swift5 --output "$OUTDIR/Images.swift"
-	swiftgen strings -t structured-swift5 "$SRCDIR/Resources/en.lproj/Localizable.strings" --output "$OUTDIR/Strings.swift"
+	swiftgen run storyboards "$SRCDIR" -t swift5 --output "$OUTDIR/Storyboards.swift"
+	swiftgen run xcassets "$SRCDIR/Resources/Images.xcassets" -t swift5 --output "$OUTDIR/Images.swift"
+	swiftgen run strings -t structured-swift5 "$SRCDIR/Resources/en.lproj/Localizable.strings" --output "$OUTDIR/Strings.swift"
 }
 
 # Main script to check if SwiftGen is installed, check the version, and run it only if version matches